Bayesian GLM Part3

Author

Murray Logan

Published

07/07/2025

1 Preparations

Load the necessary libraries

library(tidyverse) # for data wrangling etc
library(rstanarm) # for fitting models in STAN
library(cmdstanr) # for cmdstan
library(brms) # for fitting models in STAN
library(coda) # for diagnostics
library(bayesplot) # for diagnostics
library(ggmcmc) # for MCMC diagnostics
library(DHARMa) # for residual diagnostics
library(rstan) # for interfacing with STAN
library(emmeans) # for marginal means etc
library(broom) # for tidying outputs
library(tidybayes) # for more tidying outputs
library(ggeffects) # for partial plots
library(broom.mixed) # for summarising models
library(ggeffects) # for partial effects plots
library(bayestestR) # for ROPE
library(see) # for some plots
library(easystats) # for the easystats ecosystem
library(INLA) # for approximate Bayes
library(INLAutils) # for additional INLA outputs
library(patchwork) # for multiple plots
library(modelsummary) # for data and model summaries
theme_set(theme_grey()) # put the default ggplot theme back
source("helperFunctions.R")

2 Scenario

Here is a modified example from Peake and Quinn (1993). Peake and Quinn (1993) investigated the relationship between the number of individuals of invertebrates living in amongst clumps of mussels on a rocky intertidal shore and the area of those mussel clumps.

Figure 1: Mussels
Table 1: Format of peakquinn.csv data files
AREA INDIV
516.00 18
469.06 60
462.25 57
938.60 100
1357.15 48
... ...
Table 2: Description of the variables in the peakquinn data file
AREA Area of mussel clump mm2 - Predictor variable
INDIV Number of individuals found within clump - Response variable

The aim of the analysis is to investigate the relationship between mussel clump area and the number of non-mussel invertebrate individuals supported in the mussel clump.

3 Read in the data

peake <- read_csv("../data/peakquinn.csv", trim_ws = TRUE)
Rows: 25 Columns: 2
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): AREA, INDIV

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
glimpse(peake)
Rows: 25
Columns: 2
$ AREA  <dbl> 516.00, 469.06, 462.25, 938.60, 1357.15, 1773.66, 1686.01, 1786.…
$ INDIV <dbl> 18, 60, 57, 100, 48, 118, 148, 214, 225, 283, 380, 278, 338, 274…
## Explore the first 6 rows of the data
head(peake)
# A tibble: 6 × 2
   AREA INDIV
  <dbl> <dbl>
1  516     18
2  469.    60
3  462.    57
4  939.   100
5 1357.    48
6 1774.   118
str(peake)
spc_tbl_ [25 × 2] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ AREA : num [1:25] 516 469 462 939 1357 ...
 $ INDIV: num [1:25] 18 60 57 100 48 118 148 214 225 283 ...
 - attr(*, "spec")=
  .. cols(
  ..   AREA = col_double(),
  ..   INDIV = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 
peake |> datawizard::data_codebook()
peake (25 rows and 2 variables, 2 shown)

ID | Name  | Type    | Missings |          Values |  N
---+-------+---------+----------+-----------------+---
1  | AREA  | numeric | 0 (0.0%) | [462.25, 27144] | 25
---+-------+---------+----------+-----------------+---
2  | INDIV | numeric | 0 (0.0%) |      [18, 1402] | 25
------------------------------------------------------
peake |> modelsummary::datasummary_skim()
Unique Missing Pct. Mean SD Min Median Max Histogram
AREA 25 0 7802.0 8266.8 462.2 4451.7 27144.0
INDIV 25 0 446.9 378.5 18.0 338.0 1402.0

4 Exploratory data analysis

When exploring these data as part of a frequentist analysis, exploratory data analysis revealed that the both the response (counds of individuals) and predictor (mussel clump area) were skewed and the relationship between raw counds and mussel clump area was not linear. Furthermore, there was strong evidence of a relationship between mean and variance. Normalising both reponse and predictor addressed these issues. However, rather than log transform the response, it was considered more appropriate to model against a distribution that used a logarithmic link function.

The individual observations here (\(y_i\)) are the observed number of (non mussel individuals found in mussel clump \(i\). As a count, these might be expected to follow a Poisson (or perhaps negative binomial) distribution. In the case of a negative binomial, the observed count for any given mussel clump area are expected to be drawn from a negative binomial distribution with a mean of \(\lambda_i\). All the negative binomial distributions are expected to share the same degree of dispersion (\(\theta\)) - that is, the degree to which the inhabitants of mussell clumps aggregate together (or any other reason for overdispersion) is independent of mussel clump area and can be estimated as a constant across all populations.

The natural log of the expected values (\(\lambda_i\)) is modelled against a linear predictor that includes an intercept (\(\beta_0\)) and a slope (\(\beta_1\)) associated with the natural log of mussel area.

The priors on \(\beta_0\) and \(\beta_1\) should be on the natura log scale (since this will be the scale of the parameters). As starting points, we will consider the following priors:

  • \(\beta_0\): Normal prior centered at 0 with a variance of 5
  • \(\beta_1\): Normal prior centered at 0 with a variance of 2
  • \(\theta\): Exponential prior with rate 1

Model formula: \[ \begin{align} y_i &\sim{} \mathcal{NB}(\lambda_i, \theta)\\ ln(\lambda_i) &= \beta_0 + \beta_1 ln(x_i)\\ \beta_0 & \sim\mathcal{N}(6,1.5)\\ \beta_1 & \sim\mathcal{N}(0,1)\\ \theta &\sim{} \mathcal{Exp}(1)\\ OR\\ \theta &\sim{} \mathcal{\Gamma}(2,1)\\ \end{align} \]

ggplot(peake, aes(y = INDIV, x = AREA)) +
  geom_point() +
  geom_smooth()

Conclusions:

  • there is some evidence of non-homogeneity of variance (observations are more tightly clustered around the trendline for small values of AREA and the spread increases throughout the trend).
  • there is some evidence of non-linearity as evidenced by the substantial change in trajectory of the loess smoother.
  • nevertheless, there is also evidence of non-normality in both the y-axis and x-axis (there are more points at low values of both y and x). Deviations from normality can make it difficult to assess other assumptions. Often the cause of homogeneity and linearity is non-homogeneity.
ggplot(peake, aes(y = INDIV)) +
  geom_boxplot()

ggplot(peake, aes(y = AREA)) +
  geom_boxplot()

Conclusions:

  • indeed both the response (INDIV) and predictor (AREA) appear to be skewed. It is not surprising that the response is skewed as it represents counts of the number of individuals. We might expect that this should follow a Poisson distribution. Poisson distributions must be bounded by 0 at the lower end and hence as the expected value (=mean and location) of a Poisson distribution approaches 0, the distribution will become more and more asymmetrical. A the same time, the distribution would be expected to become narrower (have a smaller variance) - since in a Poisson distribution the mean and variance are equal.

Along with modelling these data against a Poisson distribution (with a natural log link), we should probably attempt to normalise the predictor variable (AREA). Whilst linear models don’t assume that the predictor variable follows a normal distribution (it is typically assumed to be a uniform distribution), it is assumed to be symmetrical. In this case, a natural log transformation might help achieve this. At the same time, it might also help homogeneity of variance and linearity.

We can mimic the action of using a log link and log-transformed predictor by presenting the scatterplot on log-transformed axes.

ggplot(peake, aes(y = INDIV, x = AREA)) +
  geom_point() +
  geom_smooth() +
  scale_y_log10() +
  scale_x_log10()

Conclusions:

  • there is no obvious violations of the assumptions now.

5 Fit the model

summary(glm(INDIV ~ log(AREA), data = peake, family = poisson()))

Call:
glm(formula = INDIV ~ log(AREA), family = poisson(), data = peake)

Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) 0.013528   0.091147   0.148    0.882    
log(AREA)   0.691628   0.009866  70.100   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for poisson family taken to be 1)

    Null deviance: 7494.5  on 24  degrees of freedom
Residual deviance: 1547.8  on 23  degrees of freedom
AIC: 1738.9

Number of Fisher Scoring iterations: 4
summary(MASS::glm.nb(INDIV ~ log(AREA), data = peake))

Call:
MASS::glm.nb(formula = INDIV ~ log(AREA), data = peake, init.theta = 7.369263003, 
    link = log)

Coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) -1.16452    0.53387  -2.181   0.0292 *  
log(AREA)    0.82469    0.06295  13.102   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for Negative Binomial(7.3693) family taken to be 1)

    Null deviance: 161.076  on 24  degrees of freedom
Residual deviance:  25.941  on 23  degrees of freedom
AIC: 312.16

Number of Fisher Scoring iterations: 1

              Theta:  7.37 
          Std. Err.:  2.16 

 2 x log-likelihood:  -306.16 

In rstanarm, the default priors are designed to be weakly informative. They are chosen to provide moderate regularlization (to help prevent overfitting) and help stabalise the computations.

peake.rstanarm <- stan_glm(INDIV ~ log(AREA),
  data = peake,
  family = poisson(),
  iter = 5000, warmup = 1000,
  chains = 3, thin = 5, refresh = 0
)
prior_summary(peake.rstanarm)
Priors for model 'peake.rstanarm' 
------
Intercept (after predictors centered)
 ~ normal(location = 0, scale = 2.5)

Coefficients
  Specified prior:
    ~ normal(location = 0, scale = 2.5)
  Adjusted prior:
    ~ normal(location = 0, scale = 2)
------
See help('prior_summary.stanreg') for more details

This tells us:

  • for the intercept, it is using a normal prior with a mean of 0 and a standard deviation of 2.5. The are the defaults for all non-Gaussian intercepts.

  • for the coefficients (in this case, just the slope), the default prior is a normal prior centered around 0 with a standard deviation of 2.5. For Poisson models, this is then adjusted for the scale of the data by dividing the 2.5 by the standard deviation of the (in this case log) predictor (then rounded).

2.5 / sd(log(peake$AREA))
[1] 2.025039
  • there is no auxillary parameter and thus there is no auxillary prior

One way to assess the priors is to have the MCMC sampler sample purely from the prior predictive distribution without conditioning on the observed data. Doing so provides a glimpse at the range of predictions possible under the priors. On the one hand, wide ranging preditions would ensure that the priors are unlikely to influence the actual preditions once they are conditioned on the data. On the other hand, if they are too wide, the sampler is being permitted to traverse into regions of parameter space that are not logically possible in the context of the actual underlying ecological context. Not only could this mean that illogical parameter estimates are possible, when the sampler is traversing regions of paramter space that are not supported by the actual data, the sampler can become unstable and have difficulty.

We can draw from the prior predictive distribution instead of conditioning on the response, by updating the model and indicating prior_PD=TRUE. After refittin the model in this way, we can plot the predictions to gain insights into the range of predictions supported by the priors alone.

peake.rstanarm1 <- update(peake.rstanarm, prior_PD = TRUE)
ggemmeans(peake.rstanarm1, ~AREA) |> plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

ggemmeans(peake.rstanarm1, ~AREA) |> plot(jitter = FALSE, show_data = TRUE) + scale_y_log10()
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
Scale for y is already present.
Adding another scale for y, which will replace the existing scale.

Conclusions:

  • we see that although the range of predictions are very wide and the slope could range from strongly negative to strongly positive, the choice to have the intercept prior have a mean of 0 results in most of the prior-only posterior draws being dramatically lower than the observed values - indeed some observed values are above the range of the prior-posterior predictions.

The following link provides some guidance about defining priors. [https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations]

When defining our own priors, we typically do not want them to be scaled.

If we wanted to define our own priors that were less vague, yet still not likely to bias the outcomes, we could try the following priors (mainly plucket out of thin air):

  • \(\beta_0\): normal centred at 6 with a standard deviation of 6
    • mean of 6: because (mean(log(peake$INDIV)))
    • sd of 2.8: because (2.5 * sd(log(peake$INDIV)))
  • \(\beta_1\): normal centred at 0 with a standard deviation of 1
    • sd of 2.3: because (sd(log(peake$INDIV))/sd(log(peake$AREA)))

I will also overlay the raw data for comparison.

peake.rstanarm2 <- stan_glm(INDIV ~ log(AREA),
  data = peake,
  family = poisson(),
  prior_intercept = normal(6, 2.8, autoscale = FALSE),
  prior = normal(0, 2.3, autoscale = FALSE),
  prior_PD = TRUE,
  iter = 5000, warmup = 1000,
  chains = 3, thin = 5, refresh = 0
)
ggemmeans(peake.rstanarm2, ~AREA) |>
  plot(show_data = TRUE) +
  scale_y_log10()
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
Scale for y is already present.
Adding another scale for y, which will replace the existing scale.

Now lets refit, conditioning on the data.

peake.rstanarm3 <- update(peake.rstanarm2, prior_PD = FALSE)
posterior_vs_prior(peake.rstanarm3,
  color_by = "vs", group_by = TRUE,
  facet_args = list(scales = "free_y")
)

Drawing from prior...

Conclusions:

  • in each case, the prior is substantially wider than the posterior, suggesting that the posterior is not biased towards the prior.
ggemmeans(peake.rstanarm3, ~AREA) |> plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

In brms, the default priors are designed to be weakly informative. They are chosen to provide moderate regularlization (to help prevent overfitting) and help stabalise the computations.

Unlike rstanarm, brms models must be compiled before they start sampling. For most models, the compilation of the stan code takes around 45 seconds.

form <- bf(INDIV ~ log(AREA), family = poisson())
peake.brm <- brm(form,
  data = peake,
  iter = 5000, warmup = 1000,
  chains = 3, cores = 3,
  thin = 5, refresh = 0,
  backend = "rstan"
)
Compiling Stan program...
Start sampling
prior_summary(peake.brm)
                  prior     class    coef group resp dpar nlpar lb ub       source
                 (flat)         b                                          default
                 (flat)         b logAREA                             (vectorized)
 student_t(3, 5.8, 2.5) Intercept                                          default

This tells us:

  • for the intercept, it is using a student t (flatter normal) prior with a mean of 5.8 and a standard deviation of 2.5. The mean of this prior is based on the median of the log-transformed response and the standard deviation of 2.5 is the default minimum.
[1] 5.823046
mad(log(peake$INDIV))
[1] 1.025465
  • for the beta coefficients (in this case, just the slope), the default prior is a inproper flat prior. A flat prior essentially means that any value between negative infinity and positive infinity are equally likely. Whilst this might seem reckless, in practice, it seems to work reasonably well for non-intercept beta parameters.

  • there is no sigma parameter in a binomial model and therefore there are no additional priors.

One way to assess the priors is to have the MCMC sampler sample purely from the prior predictive distribution without conditioning on the observed data. Doing so provides a glimpse at the range of predictions possible under the priors. On the one hand, wide ranging preditions would ensure that the priors are unlikely to influence the actual preditions once they are conditioned on the data. On the other hand, if they are too wide, the sampler is being permitted to traverse into regions of parameter space that are not logically possible in the context of the actual underlying ecological context. Not only could this mean that illogical parameter estimates are possible, when the sampler is traversing regions of paramter space that are not supported by the actual data, the sampler can become unstable and have difficulty.

In brms, we can inform the sampler to draw from the prior predictive distribution instead of conditioning on the response, by running the model with the sample_prior='only' argument. Unfortunately, this cannot be applied when there are flat priors (since the posteriors will necessarily extend to negative and positive infinity). Therefore, in order to use this useful routine, we need to make sure that we have defined a proper prior for all parameters.

We will adopt a similar approach to that of rstanarm - that is 2.5 * sd(log(peake$INDIV))/sd(log(peake$AREA)) (approx. 2.3).

form <- bf(INDIV ~ log(AREA), family = poisson())
peake.brm1 <- brm(form,
  data = peake,
  prior = c(
    prior(normal(0, 2.3), class = "b")
  ),
  sample_prior = "only",
  iter = 5000, warmup = 1000,
  chains = 3, cores = 3,
  thin = 5, refresh = 0,
  backend = "rstan"
)
Compiling Stan program...
Start sampling
ggemmeans(peake.brm1, ~AREA) |> plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

ggemmeans(peake.brm1, ~AREA) |> plot(show_data = TRUE) + scale_y_log10()
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
Scale for y is already present.
Adding another scale for y, which will replace the existing scale.

conditional_effects(peake.brm1) |>
  plot(points = TRUE) |>
  _[[1]] + scale_y_log10()

plot(conditional_effects(peake.brm1), points = TRUE, plot = FALSE)[[1]] + scale_y_log10()

Conclusions:

  • we see that the range of predictions is faily wide and the slope could range from strongly negative to strongly positive.

The following link provides some guidance about defining priors. [https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations]

When defining our own priors, we typically do not want them to be scaled.

If we wanted to define our own priors that were less vague, yet still not likely to bias the outcomes, we could try the following priors (mainly plucket out of thin air):

  • \(\beta_0\): normal centred at 6 with a standard deviation of 6
    • mean of 6: because (mean(log(peake$INDIV)))
    • sd of 1.5: because (sd(log(peake$INDIV)))
  • \(\beta_1\): normal centred at 0 with a standard deviation of 1
    • sd of 1: because (sd(log(peake$INDIV))/sd(log(peake$AREA)))

I will also overlay the raw data for comparison.

form <- bf(INDIV ~ log(AREA), family = poisson())
priors <- prior(normal(6, 1.5), class = "Intercept") +
  prior(normal(0, 1), class = "b")
peake.brm2 <- brm(form,
  data = peake,
  prior = priors,
  sample_prior = "only",
  iter = 5000, warmup = 1000,
  chains = 3, cores = 3,
  thin = 5, refresh = 0,
  backend = "rstan"
)
Compiling Stan program...
Start sampling
ggemmeans(peake.brm2, ~AREA) |>
  plot(show_data = TRUE) +
  scale_y_log10()
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
Scale for y is already present.
Adding another scale for y, which will replace the existing scale.

peake.brm3 <- update(peake.brm2, sample_prior = TRUE, refresh = 0)
The desired updates require recompiling the model
Compiling Stan program...
Start sampling
peake.brm3 |> get_variables()
 [1] "b_Intercept"     "b_logAREA"       "Intercept"       "prior_Intercept"
 [5] "prior_b"         "lprior"          "lp__"            "accept_stat__"  
 [9] "stepsize__"      "treedepth__"     "n_leapfrog__"    "divergent__"    
[13] "energy__"       
peake.brm3 |>
  hypothesis("logAREA=0") |>
  plot()

peake.brm3 |> SUYR_prior_and_posterior()

standata(peake.brm3)
$N
[1] 25

$Y
 [1]   18   60   57  100   48  118  148  214  225  283  380  278  338  274  569
[16]  509  682  600  978  363 1402  675 1159 1062  632

$K
[1] 2

$Kc
[1] 1

$X
   Intercept   logAREA
1          1  6.246107
2          1  6.150731
3          1  6.136106
4          1  6.844389
5          1  7.213142
6          1  7.480800
7          1  7.430120
8          1  7.487896
9          1  8.035949
10         1  8.289067
11         1  8.394989
12         1  8.401037
13         1  8.513765
14         1  8.400853
15         1  8.610818
16         1  8.919481
17         1  8.873303
18         1  9.121503
19         1  9.223560
20         1  9.136445
21         1  9.527275
22         1  9.918414
23         1 10.115073
24         1 10.208912
25         1 10.170373
attr(,"assign")
[1] 0 1

$prior_only
[1] 0

attr(,"class")
[1] "standata" "list"    
stancode(peake.brm3)
// generated with brms 2.22.0
functions {
}
data {
  int<lower=1> N;  // total number of observations
  array[N] int Y;  // response variable
  int<lower=1> K;  // number of population-level effects
  matrix[N, K] X;  // population-level design matrix
  int<lower=1> Kc;  // number of population-level effects after centering
  int prior_only;  // should the likelihood be ignored?
}
transformed data {
  matrix[N, Kc] Xc;  // centered version of X without an intercept
  vector[Kc] means_X;  // column means of X before centering
  for (i in 2:K) {
    means_X[i - 1] = mean(X[, i]);
    Xc[, i - 1] = X[, i] - means_X[i - 1];
  }
}
parameters {
  vector[Kc] b;  // regression coefficients
  real Intercept;  // temporary intercept for centered predictors
}
transformed parameters {
  real lprior = 0;  // prior contributions to the log posterior
  lprior += normal_lpdf(b | 0, 1);
  lprior += normal_lpdf(Intercept | 6, 1.5);
}
model {
  // likelihood including constants
  if (!prior_only) {
    target += poisson_log_glm_lpmf(Y | Xc, Intercept, b);
  }
  // priors including constants
  target += lprior;
}
generated quantities {
  // actual population-level intercept
  real b_Intercept = Intercept - dot_product(means_X, b);
  // additionally sample draws from priors
  real prior_b = normal_rng(0,1);
  real prior_Intercept = normal_rng(6,1.5);
}

6 MCMC sampling diagnostics

The bayesplot package offers a range of MCMC diagnostics as well as Posterior Probability Checks (PPC), all of which have a convenient plot() interface. Lets start with the MCMC diagnostics.

See list of available diagnostics by name
available_mcmc()
bayesplot MCMC module:
  mcmc_acf
  mcmc_acf_bar
  mcmc_areas
  mcmc_areas_ridges
  mcmc_combo
  mcmc_dens
  mcmc_dens_chains
  mcmc_dens_overlay
  mcmc_hex
  mcmc_hist
  mcmc_hist_by_chain
  mcmc_intervals
  mcmc_neff
  mcmc_neff_hist
  mcmc_nuts_acceptance
  mcmc_nuts_divergence
  mcmc_nuts_energy
  mcmc_nuts_stepsize
  mcmc_nuts_treedepth
  mcmc_pairs
  mcmc_parcoord
  mcmc_rank_ecdf
  mcmc_rank_hist
  mcmc_rank_overlay
  mcmc_recover_hist
  mcmc_recover_intervals
  mcmc_recover_scatter
  mcmc_rhat
  mcmc_rhat_hist
  mcmc_scatter
  mcmc_trace
  mcmc_trace_highlight
  mcmc_violin

Of these, we will focus on:

  • mcmc_trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different shade of blue, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
plot(peake.rstanarm3, plotfun = "mcmc_trace")

The chains appear well mixed and very similar

  • acf (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
plot(peake.rstanarm3, "acf_bar")

There is no evidence of autocorrelation in the MCMC samples

  • Rhat: Rhat is a measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
plot(peake.rstanarm3, "rhat_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • neff (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

plot(peake.rstanarm3, "neff_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

More diagnostics
plot(peake.rstanarm3, "combo")

plot(peake.rstanarm3, "violin")

The rstan package offers a range of MCMC diagnostics. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • stan_trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
stan_trace(peake.rstanarm3)

The chains appear well mixed and very similar

  • stan_acf (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
stan_ac(peake.rstanarm3)

There is no evidence of autocorrelation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
stan_rhat(peake.rstanarm3)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

stan_ess(peake.rstanarm3)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

stan_dens(peake.rstanarm3, separate_chains = TRUE)

The ggmean package also has a set of MCMC diagnostic functions. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • ggs_traceplot: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
peake.ggs <- ggs(peake.rstanarm3)
ggs_traceplot(peake.ggs)

The chains appear well mixed and very similar

  • gss_autocorrelation (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
ggs_autocorrelation(peake.ggs)

There is no evidence of autocorrelation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
ggs_Rhat(peake.ggs)

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

ggs_effective(peake.ggs)
Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
dplyr 1.1.0.
ℹ Please use `reframe()` instead.
ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
  always returns an ungrouped data frame and adjust accordingly.
ℹ The deprecated feature was likely used in the ggmcmc package.
  Please report the issue at <https://github.com/xfim/ggmcmc/issues/>.

Ratios all very high.

More diagnostics
ggs_crosscorrelation(peake.ggs)

ggs_grb(peake.ggs)

The bayesplot package offers a range of MCMC diagnostics as well as Posterior Probability Checks (PPC), all of which have a convenient plot() interface. Lets start with the MCMC diagnostics.

See list of available diagnostics by name
available_mcmc()
bayesplot MCMC module:
  mcmc_acf
  mcmc_acf_bar
  mcmc_areas
  mcmc_areas_ridges
  mcmc_combo
  mcmc_dens
  mcmc_dens_chains
  mcmc_dens_overlay
  mcmc_hex
  mcmc_hist
  mcmc_hist_by_chain
  mcmc_intervals
  mcmc_neff
  mcmc_neff_hist
  mcmc_nuts_acceptance
  mcmc_nuts_divergence
  mcmc_nuts_energy
  mcmc_nuts_stepsize
  mcmc_nuts_treedepth
  mcmc_pairs
  mcmc_parcoord
  mcmc_rank_ecdf
  mcmc_rank_hist
  mcmc_rank_overlay
  mcmc_recover_hist
  mcmc_recover_intervals
  mcmc_recover_scatter
  mcmc_rhat
  mcmc_rhat_hist
  mcmc_scatter
  mcmc_trace
  mcmc_trace_highlight
  mcmc_violin

Of these, we will focus on:

  • trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different shade of blue, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
mcmc_plot(peake.brm3, type = "trace")
No divergences to plot.

The chains appear well mixed and very similar

  • acf_bar (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
mcmc_plot(peake.brm3, type = "acf_bar")

There is no evidence of autocorrelation in the MCMC samples

  • rhat_hist: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
mcmc_plot(peake.brm3, type = "rhat_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • neff_hist (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

mcmc_plot(peake.brm3, type = "neff_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

More diagnostics
mcmc_plot(peake.brm3, type = "combo")

mcmc_plot(peake.brm3, type = "violin")

The rstan package offers a range of MCMC diagnostics. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • stan_trace: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
stan_trace(peake.brm3$fit)

The chains appear well mixed and very similar

  • stan_acf (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
stan_ac(peake.brm3$fit)

There is no evidence of autocorrelation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
stan_rhat(peake.brm3$fit)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

stan_ess(peake.brm3$fit)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Ratios all very high.

stan_dens(peake.brm3$fit, separate_chains = TRUE)

The ggmean package also has a set of MCMC diagnostic functions. Lets start with the MCMC diagnostics.

Of these, we will focus on:

  • ggs_traceplot: this plots the estimates of each parameter over the post-warmup length of each MCMC chain. Each chain is plotted in a different colour, with each parameter in its own facet. Ideally, each trace should just look like noise without any discernible drift and each of the traces for a specific parameter should look the same (i.e, should not be displaced above or below any other trace for that parameter).
peake.ggs <- ggs(peake.brm3)
ggs_traceplot(peake.ggs)

The chains appear well mixed and very similar

  • gss_autocorrelation (autocorrelation function): plots the autocorrelation between successive MCMC sample lags for each parameter and each chain
ggs_autocorrelation(peake.ggs)

There is no evidence of autocorrelation in the MCMC samples

  • stan_rhat: Rhat is a scale reduction factor measure of convergence between the chains. The closer the values are to 1, the more the chains have converged. Values greater than 1.05 indicate a lack of convergence. There will be an Rhat value for each parameter estimated.
ggs_Rhat(peake.ggs)

All Rhat values are below 1.05, suggesting the chains have converged.

  • stan_ess (number of effective samples): the ratio of the number of effective samples (those not rejected by the sampler) to the number of samples provides an indication of the effectiveness (and efficiency) of the MCMC sampler. Ratios that are less than 0.5 for a parameter suggest that the sampler spent considerable time in difficult areas of the sampling domain and rejected more than half of the samples (replacing them with the previous effective sample).

    If the ratios are low, tightening the priors may help.

ggs_effective(peake.ggs)

Ratios all very high.

More diagnostics
ggs_crosscorrelation(peake.ggs)

ggs_grb(peake.ggs)

7 Model validation

Post predictive checks provide additional diagnostics about the fit of the model. Specifically, they provide a comparison between predictions drawn from the model and the observed data used to train the model.

See list of available diagnostics by name
available_ppc()
bayesplot PPC module:
  ppc_bars
  ppc_bars_grouped
  ppc_boxplot
  ppc_dens
  ppc_dens_overlay
  ppc_dens_overlay_grouped
  ppc_ecdf_overlay
  ppc_ecdf_overlay_grouped
  ppc_error_binned
  ppc_error_hist
  ppc_error_hist_grouped
  ppc_error_scatter
  ppc_error_scatter_avg
  ppc_error_scatter_avg_grouped
  ppc_error_scatter_avg_vs_x
  ppc_freqpoly
  ppc_freqpoly_grouped
  ppc_hist
  ppc_intervals
  ppc_intervals_grouped
  ppc_km_overlay
  ppc_km_overlay_grouped
  ppc_loo_intervals
  ppc_loo_pit_ecdf
  ppc_loo_pit_overlay
  ppc_loo_pit_qq
  ppc_loo_ribbon
  ppc_pit_ecdf
  ppc_pit_ecdf_grouped
  ppc_ribbon
  ppc_ribbon_grouped
  ppc_rootogram
  ppc_scatter
  ppc_scatter_avg
  ppc_scatter_avg_grouped
  ppc_stat
  ppc_stat_2d
  ppc_stat_freqpoly
  ppc_stat_freqpoly_grouped
  ppc_stat_grouped
  ppc_violin_grouped
  • dens_overlay: plots the density distribution of the observed data (black line) overlayed ontop of 50 density distributions generated from draws from the model (light blue). Ideally, the 50 realisations should be roughly consistent with the observed data.
pp_check(peake.rstanarm3, plotfun = "dens_overlay")

The model draws appear deviate from the observed data.

  • error_scatter_avg: this plots the observed values against the average residuals. Similar to a residual plot, we do not want to see any patterns in this plot. There is some pattern remaining in these residuals.
pp_check(peake.rstanarm3, plotfun = "error_scatter_avg")

The predictive error seems to be related to the predictor - the model performs poorest at higher mussel clump areas.

  • error_scatter_avg_vs_x: this is similar to a regular residual plot and as such should be interpreted as such. Again, this is not interpretable for binary data.
pp_check(peake.rstanarm3, x = peake$AREA, plotfun = "error_scatter_avg_vs_x")

  • intervals: plots the observed data overlayed ontop of posterior predictions associated with each level of the predictor. Ideally, the observed data should all fall within the predictive intervals.
pp_check(peake.rstanarm3, x = peake$AREA, plotfun = "intervals")

The modelled preditions seem to underestimate the uncertainty with increasing mussel clump area.

  • ribbon: this is just an alternative way of expressing the above plot.
pp_check(peake.rstanarm3, x = peake$AREA, plotfun = "ribbon")

The shinystan package allows the full suite of MCMC diagnostics and posterior predictive checks to be accessed via a web interface.

# library(shinystan)
# launch_shinystan(peake.rstanarm3)

DHARMa residuals provide very useful diagnostics. Unfortunately, we cannot directly use the simulateResiduals() function to generate the simulated residuals. However, if we are willing to calculate some of the components ourself, we can still obtain the simulated residuals from the fitted stan model.

We need to supply:

  • simulated (predicted) responses associated with each observation.
  • observed values
  • fitted (predicted) responses (averaged) associated with each observation
preds <- posterior_predict(peake.rstanarm3, ndraws = 250, summary = FALSE)
peake.resids <- createDHARMa(
  simulatedResponse = t(preds),
  observedResponse = peake$INDIV,
  fittedPredictedResponse = apply(preds, 2, median),
  integerResponse = TRUE
)
plot(peake.resids)

peake.resids |> testDispersion()


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 95.364, p-value < 2.2e-16
alternative hypothesis: two.sided

Conclusions:

  • the simulated residuals suggest a general lack of fit due to overdispersion and outliers
  • perhaps we should explore a negative binomial model

Post predictive checks provide additional diagnostics about the fit of the model. Specifically, they provide a comparison between predictions drawn from the model and the observed data used to train the model.

See list of available diagnostics by name
available_ppc()
bayesplot PPC module:
  ppc_bars
  ppc_bars_grouped
  ppc_boxplot
  ppc_dens
  ppc_dens_overlay
  ppc_dens_overlay_grouped
  ppc_ecdf_overlay
  ppc_ecdf_overlay_grouped
  ppc_error_binned
  ppc_error_hist
  ppc_error_hist_grouped
  ppc_error_scatter
  ppc_error_scatter_avg
  ppc_error_scatter_avg_grouped
  ppc_error_scatter_avg_vs_x
  ppc_freqpoly
  ppc_freqpoly_grouped
  ppc_hist
  ppc_intervals
  ppc_intervals_grouped
  ppc_km_overlay
  ppc_km_overlay_grouped
  ppc_loo_intervals
  ppc_loo_pit_ecdf
  ppc_loo_pit_overlay
  ppc_loo_pit_qq
  ppc_loo_ribbon
  ppc_pit_ecdf
  ppc_pit_ecdf_grouped
  ppc_ribbon
  ppc_ribbon_grouped
  ppc_rootogram
  ppc_scatter
  ppc_scatter_avg
  ppc_scatter_avg_grouped
  ppc_stat
  ppc_stat_2d
  ppc_stat_freqpoly
  ppc_stat_freqpoly_grouped
  ppc_stat_grouped
  ppc_violin_grouped
  • dens_overlay: plots the density distribution of the observed data (black line) overlayed ontop of 50 density distributions generated from draws from the model (light blue). Ideally, the 50 realisations should be roughly consistent with the observed data.
pp_check(peake.brm3, type = "dens_overlay", ndraws = 100)

The model draws appear deviate from the observed data.

  • error_scatter_avg: this plots the observed values against the average residuals. Similar to a residual plot, we do not want to see any patterns in this plot. There is some pattern remaining in these residuals.
pp_check(peake.brm3, type = "error_scatter_avg")
Using all posterior draws for ppc type 'error_scatter_avg' by default.

The predictive error seems to be related to the predictor - the model performs poorest at higher mussel clump areas.

  • error_scatter_avg_vs_x: this is similar to a regular residual plot and as such should be interpreted as such. Again, this is not interpretable for binary data.
pp_check(peake.brm3, x = "AREA", type = "error_scatter_avg_vs_x")
Using all posterior draws for ppc type 'error_scatter_avg_vs_x' by default.

  • intervals: plots the observed data overlayed ontop of posterior predictions associated with each level of the predictor. Ideally, the observed data should all fall within the predictive intervals.
pp_check(peake.brm3, x = "AREA", type = "intervals")
Using all posterior draws for ppc type 'intervals' by default.

The modelled preditions seem to underestimate the uncertainty with increasing mussel clump area.

  • ribbon: this is just an alternative way of expressing the above plot.
pp_check(peake.brm3, x = "AREA", type = "ribbon")
Using all posterior draws for ppc type 'ribbon' by default.

The shinystan package allows the full suite of MCMC diagnostics and posterior predictive checks to be accessed via a web interface.

# library(shinystan)
# launch_shinystan(peake.brm3)

DHARMa residuals provide very useful diagnostics. Unfortunately, we cannot directly use the simulateResiduals() function to generate the simulated residuals. However, if we are willing to calculate some of the components ourself, we can still obtain the simulated residuals from the fitted stan model.

We need to supply:

  • simulated (predicted) responses associated with each observation.
  • observed values
  • fitted (predicted) responses (averaged) associated with each observation
preds <- posterior_predict(peake.brm3, ndraws = 250, summary = FALSE)
peake.resids <- createDHARMa(
  simulatedResponse = t(preds),
  observedResponse = peake$INDIV,
  fittedPredictedResponse = apply(preds, 2, median),
  integerResponse = TRUE
)
plot(peake.resids)

peake.resids |> testDispersion()


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 93.043, p-value < 2.2e-16
alternative hypothesis: two.sided
peake.resids <- make_brms_dharma_res(peake.brm3, integerResponse = FALSE)
wrap_elements(~ testUniformity(peake.resids)) +
  wrap_elements(~ plotResiduals(peake.resids, form = factor(rep(1, nrow(peake))))) +
  wrap_elements(~ plotResiduals(peake.resids, quantreg = FALSE)) +
  wrap_elements(~ testDispersion(peake.resids))

Conclusions:

  • the simulated residuals suggest a general lack of fit due to overdispersion and outliers
  • perhaps we should explore a negative binomial model

8 Fit Negative Binomial rstanarm

peake.rstanarm4 <- stan_glm(INDIV ~ log(AREA),
  data = peake,
  family = neg_binomial_2(),
  prior_intercept = normal(6, 2.8, autoscale = FALSE),
  prior = normal(0, 2.3, autoscale = FALSE),
  prior_aux = rstanarm::exponential(rate = 1, autoscale = FALSE),
  iter = 5000, warmup = 1000,
  chains = 3, thin = 5, refresh = 0
)
posterior_vs_prior(peake.rstanarm4,
  color_by = "vs", group_by = TRUE,
  facet_args = list(scales = "free_y")
)

Drawing from prior...

Conclusions:

  • in each case, the prior is substantially wider than the posterior, suggesting that the posterior is not biased towards the prior.
## ggemmeans(peake.rstanarm4,  ~AREA) |> plot(show_data=TRUE)
ggpredict(peake.rstanarm4, ~AREA) |> plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

plot(peake.rstanarm4, plotfun = "mcmc_trace")

plot(peake.rstanarm4, "acf_bar")

plot(peake.rstanarm4, "rhat_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

plot(peake.rstanarm4, "neff_hist")
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

pp_check(peake.rstanarm4, x = peake$AREA, plotfun = "dens_overlay")
Warning: The following arguments were unrecognized and ignored: x

pp_check(peake.rstanarm4, x = peake$AREA, plotfun = "error_scatter_avg_vs_x")

pp_check(peake.rstanarm4, x = peake$AREA, plotfun = "intervals")

preds <- posterior_predict(peake.rstanarm4, ndraws = 250, summary = FALSE)
peake.resids <- createDHARMa(
  simulatedResponse = t(preds),
  observedResponse = peake$INDIV,
  fittedPredictedResponse = apply(preds, 2, median),
  integerResponse = TRUE
)
plot(peake.resids)

Different models can be compared via information criterion. LOO (Leave One Out) IC is similar to AIC except that AIC does not consider priors and assumes that the posterior likelihood is multivariate normal. LOO AIC does not and integrates over all uncertainty.

  • ELPD: is the theoretical expected log pointwise predictive density for a new dataset and can be estimated via leave-one-out cross-validation.
  • elpd_loo: is the Bayesian LOO estimate of ELPD
  • SE of elpd_loo: is the Monte Carlo standard error is the estimate for the computational accuracy of MCMC and importance sampling used to compute elpd_loo
  • p_loo: is the difference between elpd_loo and the estimated from non-cross validated ELPD and therefore is a measure of the relative extra difficulty of predicting new data compared to the observed data. It can also be a meaure of the effective number of parameters:
    • in a well behaved model, p_loo will be less than the number of estimated parameters and the total sample size.
    • in a misspecified model, p_loo will be greater than the number of estimated parameters and the total sample size.
  • Pareto k: is a relative measure of the influence of each observation as well as the accuracy with which the associated leave one out calculations can be estimated.
    • when k < 0.5: the corresponding components can be accurately estimated
    • when 0.5 < k < 0.7: the accuracy is lower but still acceptable
    • when k>0.7: the accuracy is too low and elpd_loo is unreliable. This can also suggest a misspecified model.

More information about the above can be gleaned from [https://mc-stan.org/loo/reference/loo-glossary.html].

Start with the Poisson model

(peake.rstanarm3.loo <- loo(peake.rstanarm3))
Warning: Found 7 observation(s) with a pareto_k > 0.7. We recommend calling 'loo' again with argument 'k_threshold = 0.7' in order to calculate the ELPD without the assumption that these observations are negligible. This will refit the model 7 times to compute the ELPDs for the problematic observations directly.

Computed from 2400 by 25 log-likelihood matrix.

         Estimate    SE
elpd_loo   -929.1 297.9
p_loo       114.1  44.4
looic      1858.2 595.8
------
MCSE of elpd_loo is NA.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.8, 1.0]).

Pareto k diagnostic values:
                         Count Pct.    Min. ESS
(-Inf, 0.7]   (good)     18    72.0%   51      
   (0.7, 1]   (bad)       3    12.0%   <NA>    
   (1, Inf)   (very bad)  4    16.0%   <NA>    
See help('pareto-k-diagnostic') for details.

Conclusions:

  • p_loo is substantially higher than the number of estimated parameters
  • there are some Pareto k values identified as bad or even very bad

Now for the Negative Binomial model

(peake.rstanarm4.loo <- loo(peake.rstanarm4))

Computed from 2400 by 25 log-likelihood matrix.

         Estimate   SE
elpd_loo   -156.7  5.4
p_loo         1.9  0.5
looic       313.4 10.7
------
MCSE of elpd_loo is 0.0.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.9, 1.1]).

All Pareto k estimates are good (k < 0.7).
See help('pareto-k-diagnostic') for details.

Conclusions:

  • p_loo is lower than the number of estimated parameters
  • there are no bad Pareto k values

We can also compare the models.

The difference in Expected Log Pointwise Probability Density (elpd_dff) computes the difference in elpd_loo of two models (expressed relative to the higher model). The difference in elpd_diff will be negative if the expected out-of-sample predictive accuracy of the first model is higher. If the difference is be positive then the second model is preferred.

Note, the above assumes that both models are valid. In the current case, we know that the Poisson model was overdispersed and thus it should not be a genuine candidate. Nevertheless, we will compare the Poisson to the Negative Binomial for illustrative purposes.

loo_compare(peake.rstanarm3.loo, peake.rstanarm4.loo)
                elpd_diff se_diff
peake.rstanarm4    0.0       0.0 
peake.rstanarm3 -772.4     294.3 

Conclusions:

  • the elpd_diff is negative, indicating that the first model (Negative Binomial) is better.
form <- bf(INDIV ~ log(AREA), family = negbinomial())
priors <-
  prior(normal(6, 1.5), class = "Intercept") +
  prior(normal(0, 1), class = "b") +
  ## prior(gamma(0.01, 0.01), class='shape')
  prior(gamma(2, 1), class = "shape")
peake.brm4 <- brm(form,
  data = peake,
  prior = priors,
  sample_prior = TRUE,
  iter = 5000, warmup = 1000,
  chains = 3, cores = 3,
  thin = 5, refresh = 0,
  backend = "rstan"
)
Compiling Stan program...
Start sampling
peake.brm4 |> SUYR_prior_and_posterior()

Conclusions:

  • in each case, the prior is substantially wider than the posterior, suggesting that the posterior is not biased towards the prior.
## ggemmeans(peake.brm4,  ~AREA) |> plot(show_data=TRUE)
ggpredict(peake.brm4, ~AREA) |> plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

peake.brm4$fit |> stan_trace()

peake.brm4$fit |> stan_ac()

peake.brm4$fit |> stan_rhat()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

peake.brm4$fit |> stan_ess()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

pp_check(peake.brm4, x = "AREA", type = "dens_overlay", ndraws = 100)
Warning: The following arguments were unrecognized and ignored: x

pp_check(peake.brm4, x = "AREA", type = "intervals")
Using all posterior draws for ppc type 'intervals' by default.

preds <- posterior_predict(peake.brm4, ndraws = 250, summary = FALSE)
peake.resids <- createDHARMa(
  simulatedResponse = t(preds),
  observedResponse = peake$INDIV,
  fittedPredictedResponse = apply(preds, 2, median),
  integerResponse = TRUE
)
plot(peake.resids)
Warning in newton(lsp = lsp, X = G$X, y = G$y, Eb = G$Eb, UrS = G$UrS, L = G$L,
: Fitting terminated with step failure - check results carefully

peake.resids <- make_brms_dharma_res(peake.brm4, integerResponse = FALSE)
wrap_elements(~ testUniformity(peake.resids)) +
  wrap_elements(~ plotResiduals(peake.resids, form = factor(rep(1, nrow(peake))))) +
  wrap_elements(~ plotResiduals(peake.resids, quantreg = FALSE)) +
  wrap_elements(~ testDispersion(peake.resids))

Different models can be compared via information criterion. LOO (Leave One Out) IC is similar to AIC except that AIC does not consider priors and assumes that the posterior likelihood is multivariate normal. LOO AIC does not and integrates over all uncertainty.

  • ELPD: is the theoretical expected log pointwise predictive density for a new dataset and can be estimated via leave-one-out cross-validation.
  • elpd_loo: is the Bayesian LOO estimate of ELPD
  • SE of elpd_loo: is the Monte Carlo standard error is the estimate for the computational accuracy of MCMC and importance sampling used to compute elpd_loo
  • p_loo: is the difference between elpd_loo and the estimated from non-cross validated ELPD and therefore is a measure of the relative extra difficulty of predicting new data compared to the observed data. It can also be a meaure of the effective number of parameters:
    • in a well behaved model, p_loo will be less than the number of estimated parameters and the total sample size.
    • in a misspecified model, p_loo will be greater than the number of estimated parameters and the total sample size.
  • Pareto k: is a relative measure of the influence of each observation as well as the accuracy with which the associated leave one out calculations can be estimated.
    • when k < 0.5: the corresponding components can be accurately estimated
    • when 0.5 < k < 0.7: the accuracy is lower but still acceptable
    • when k>0.7: the accuracy is too low and elpd_loo is unreliable. This can also suggest a misspecified model.

More information about the above can be gleaned from [https://mc-stan.org/loo/reference/loo-glossary.html].

Start with the Poisson model

(peake.brm3.loo <- loo(peake.brm3))
Warning: Found 6 observations with a pareto_k > 0.7 in model 'peake.brm3'. We
recommend to set 'moment_match = TRUE' in order to perform moment matching for
problematic observations.

Computed from 2400 by 25 log-likelihood matrix.

         Estimate    SE
elpd_loo   -932.8 299.7
p_loo       117.4  45.3
looic      1865.5 599.3
------
MCSE of elpd_loo is NA.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.9, 1.0]).

Pareto k diagnostic values:
                         Count Pct.    Min. ESS
(-Inf, 0.7]   (good)     19    76.0%   66      
   (0.7, 1]   (bad)       2     8.0%   <NA>    
   (1, Inf)   (very bad)  4    16.0%   <NA>    
See help('pareto-k-diagnostic') for details.

Conclusions:

  • p_loo is substantially higher than the number of estimated parameters
  • there are some Pareto k values identified as bad or even very bad

Now for the Negative Binomial model

(peake.brm4.loo <- loo(peake.brm4))

Computed from 2400 by 25 log-likelihood matrix.

         Estimate   SE
elpd_loo   -156.5  5.4
p_loo         2.1  0.5
looic       313.1 10.8
------
MCSE of elpd_loo is 0.0.
MCSE and ESS estimates assume MCMC draws (r_eff in [0.8, 1.0]).

All Pareto k estimates are good (k < 0.7).
See help('pareto-k-diagnostic') for details.

Conclusions:

  • p_loo is lower than the number of estimated parameters
  • there are no bad Pareto k values

We can also compare the models.

The difference in Expected Log Pointwise Probability Density (elpd_dff) computes the difference in elpd_loo of two models (expressed relative to the higher model). The difference in elpd_diff will be negative if the expected out-of-sample predictive accuracy of the first model is higher. If the difference is be positive then the second model is preferred.

Note, the above assumes that both models are valid. In the current case, we know that the Poisson model was overdispersed and thus it should not be a genuine candidate. Nevertheless, we will compare the Poisson to the Negative Binomial for illustrative purposes.

loo_compare(peake.brm3.loo, peake.brm4.loo)
           elpd_diff se_diff
peake.brm4    0.0       0.0 
peake.brm3 -776.2     296.0 

Conclusions:

  • the elpd_diff is negative, indicating that the first model (Negative Binomial) is better.

9 Partial effects plots

peake.rstanarm4 |>
  ggpredict() |>
  plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

## Note, does not seem to backtransform...
peake.rstanarm4 |>
  ggemmeans(~AREA, type = "fixed", back.transform = TRUE) |>
  plot()

peake.rstanarm4 |>
  epred_draws(newdata = peake) |>
  median_hdci() |>
  ggplot(aes(x = AREA, y = .epred)) +
  geom_ribbon(aes(ymin = .lower, ymax = .upper), fill = "blue", alpha = 0.3) +
  geom_line() +
  geom_point(data = peake, aes(y = INDIV, x = AREA))

peake.brm4 |>
  conditional_effects() |>
  plot(points = TRUE)

peake.brm4 |>
  conditional_effects(spaghetti = TRUE, ndraws = 200) |>
  plot(points = TRUE) + theme_classic()

NULL
ce <- peake.brm4 |> conditional_effects(spaghetti = TRUE, ndraws = 200)
plot(ce, points = TRUE, plot = FALSE)[[1]] + theme_classic()

peake.brm4 |>
  ggpredict() |>
  plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

peake.brm4 |>
  ggemmeans(~AREA) |>
  plot(show_data = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

peake.brm4 |>
  epred_draws(newdata = peake) |>
  median_hdci() |>
  ggplot(aes(x = AREA, y = .epred)) +
  geom_ribbon(aes(ymin = .lower, ymax = .upper), fill = "blue", alpha = 0.3) +
  geom_line() +
  geom_point(data = peake, aes(y = INDIV, x = AREA))

10 Model investigation

The summary() method generates simple summaries (mean, standard deviation as well as 10, 50 and 90 percentiles).

summary(peake.rstanarm4)

Model Info:
 function:     stan_glm
 family:       neg_binomial_2 [log]
 formula:      INDIV ~ log(AREA)
 algorithm:    sampling
 sample:       2400 (posterior sample size)
 priors:       see help('prior_summary')
 observations: 25
 predictors:   2

Estimates:
                        mean   sd   10%   50%   90%
(Intercept)           -1.2    0.7 -2.1  -1.2  -0.3 
log(AREA)              0.8    0.1  0.7   0.8   0.9 
reciprocal_dispersion  4.6    1.3  3.1   4.5   6.4 

Fit Diagnostics:
           mean   sd    10%   50%   90%
mean_PPD 483.1   89.5 377.1 475.0 596.0

The mean_ppd is the sample average posterior predictive distribution of the outcome variable (for details see help('summary.stanreg')).

MCMC diagnostics
                      mcse Rhat n_eff
(Intercept)           0.0  1.0  1962 
log(AREA)             0.0  1.0  1998 
reciprocal_dispersion 0.0  1.0  2402 
mean_PPD              1.8  1.0  2402 
log-posterior         0.0  1.0  2378 

For each parameter, mcse is Monte Carlo standard error, n_eff is a crude measure of effective sample size, and Rhat is the potential scale reduction factor on split chains (at convergence Rhat=1).

Conclusions:

  • in the Model Info, we are informed that the total MCMC posterior sample size is 2400 and that there were 19 raw observations.
  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.17. This is the mean of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.31. When the mussel clump is 0, we expect to find 0.31 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.83 (mean) or 0.72 (median) with a standard deviation of 0. The 90% credibility intervals indicate that we are 90% confident that the slope is between 0.83 and 0.83 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.28. This represents a 128% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.rstanarm4$stanfit |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 5 × 8
  variable               median    lower    upper  rhat length ess_bulk ess_tail
  <chr>                   <dbl>    <dbl>    <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 (Intercept)            -1.17    -2.63     0.181  1.00   2400    1971.    2268.
2 log(AREA)               0.827    0.675    1.01   1.00   2400    2017.    2232.
3 reciprocal_dispersi…    4.49     2.31     7.18   1.00   2400    2392.    2212.
4 mean_PPD              475.     303.     647.     1.00   2400    2418.    2314.
5 log-posterior        -161.    -164.    -160.     1.00   2400    2303.    2347.

We can also alter the CI level.

peake.rstanarm4$stanfit |>
  summarise_draws(
    median,
    ~ HDInterval::hdi(.x, credMass = 0.9),
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 5 × 8
  variable               median    lower    upper  rhat length ess_bulk ess_tail
  <chr>                   <dbl>    <dbl>    <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 (Intercept)            -1.17    -2.37  -5.91e-2  1.00   2400    1971.    2268.
2 log(AREA)               0.827    0.682  9.56e-1  1.00   2400    2017.    2232.
3 reciprocal_dispersi…    4.49     2.53   6.62e+0  1.00   2400    2392.    2212.
4 mean_PPD              475.     331.     6.10e+2  1.00   2400    2418.    2314.
5 log-posterior        -161.    -163.    -1.60e+2  1.00   2400    2303.    2347.

Arguably, it would be better to back-transform to the ratio scale

peake.rstanarm4$stanfit |>
  ## tidy_draws() |>
  ## exp() |>
  summarise_draws(
    ~ median(exp(.x)),
    ~ HDInterval::hdi(exp(.x)),
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 5 × 8
  variable `~median(exp(.x))`     lower     upper  rhat length ess_bulk ess_tail
  <chr>                 <dbl>     <dbl>     <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 (Interc…          3.09e-  1 2.62e-  2 1.00e+  0  1.00   2400    1971.    2268.
2 log(ARE…          2.29e+  0 1.95e+  0 2.71e+  0  1.00   2400    2017.    2232.
3 recipro…          8.94e+  1 3.95e+  0 1.11e+  3  1.00   2400    2392.    2212.
4 mean_PPD          2.04e+206 2.01e+109 3.46e+278  1.00   2400    2418.    2314.
5 log-pos…          7.96e- 71 3.91e- 74 2.26e- 70  1.00   2400    2303.    2347.
tidyMCMC(peake.rstanarm4$stanfit, estimate.method = "median", conf.int = TRUE, conf.method = "HPDinterval", rhat = TRUE, ess = TRUE)
# A tibble: 5 × 7
  term                  estimate std.error conf.low conf.high  rhat   ess
  <chr>                    <dbl>     <dbl>    <dbl>     <dbl> <dbl> <int>
1 (Intercept)             -1.17     0.714    -2.63      0.181 1.00   1962
2 log(AREA)                0.826    0.0846    0.675     1.01  1.00   1998
3 reciprocal_dispersion    4.63     1.29      2.31      7.18  1.00   2402
4 mean_PPD               483.      89.5     303.      647.    1.00   2402
5 log-posterior         -162.       1.26   -164.     -160.    1.000  2378

Conclusions:

  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.17. This is the median of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.31. When the mussel clump is 0, we expect to find 0.31 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.83 (mean) with a standard error of 0.08. The 95% credibility intervals indicate that we are 90% confident that the slope is between 0.68 and 1.01 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.28. This represents a 128% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.rstanarm4$stanfit |> as_draws_df()
# A draws_df: 800 iterations, 3 chains, and 5 variables
   (Intercept) log(AREA) reciprocal_dispersion mean_PPD log-posterior
1        -1.33      0.86                   3.5      441          -162
2        -1.54      0.87                   4.6      451          -160
3        -1.64      0.89                   7.1      572          -162
4        -1.05      0.80                   3.0      373          -162
5        -1.05      0.83                   4.2      517          -162
6        -1.46      0.85                   6.6      443          -161
7        -0.21      0.72                   5.2      424          -161
8        -1.07      0.81                   3.6      415          -161
9        -1.29      0.84                   4.3      469          -160
10       -0.65      0.76                   4.2      450          -161
# ... with 2390 more draws
# ... hidden reserved variables {'.chain', '.iteration', '.draw'}
## summarised
peake.rstanarm4$stanfit |>
  as_draws_df() |>
  exp() |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat,
    ess_bulk, ess_tail
  )
# A tibble: 5 × 7
  variable                 median     lower     upper  rhat ess_bulk ess_tail
  <chr>                     <dbl>     <dbl>     <dbl> <dbl>    <dbl>    <dbl>
1 (Intercept)           3.09e-  1 2.62e-  2 1.00e+  0  1.00    1971.    2268.
2 log(AREA)             2.29e+  0 1.95e+  0 2.71e+  0  1.00    2017.    2232.
3 reciprocal_dispersion 8.94e+  1 3.95e+  0 1.11e+  3  1.00    2392.    2212.
4 mean_PPD              2.04e+206 2.01e+109 3.46e+278  1.00    2426.    2314.
5 log-posterior         7.96e- 71 3.91e- 74 2.26e- 70  1.00    2303.    2347.
## summarised on fractional scale
peake.rstanarm4$stanfit |>
  as_draws_df() |>
  dplyr::select(matches("Intercept|AREA")) |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat,
    length,
    ess_bulk, ess_tail
  )
Warning: Dropping 'draws_df' class as required metadata was removed.
# A tibble: 2 × 8
  variable    median  lower upper  rhat length ess_bulk ess_tail
  <chr>        <dbl>  <dbl> <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 (Intercept) -1.17  -2.63  0.181 1.000   2400    1954.    2254.
2 log(AREA)    0.827  0.675 1.01  1.000   2400    1997.    2217.
## OR
names <- peake.rstanarm4 |>
  formula() |>
  model.matrix(peake) |>
  colnames()

peake.rstanarm4$stanfit |>
  as_draws_df() |>
  dplyr::select(any_of(names)) |>
  mutate(across(everything(), exp)) |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat,
    length,
    ess_bulk, ess_tail
  )
Warning: Dropping 'draws_df' class as required metadata was removed.
# A tibble: 2 × 8
  variable    median  lower upper  rhat length ess_bulk ess_tail
  <chr>        <dbl>  <dbl> <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 (Intercept)  0.309 0.0262  1.00 1.000   2400    1954.    2254.
2 log(AREA)    2.29  1.95    2.71 1.000   2400    1997.    2217.

Due to the presence of a log transform in the predictor, it is better to use the regex version.

peake.rstanarm4 |> get_variables()
[1] "(Intercept)"           "log(AREA)"             "reciprocal_dispersion"
[4] "accept_stat__"         "stepsize__"            "treedepth__"          
[7] "n_leapfrog__"          "divergent__"           "energy__"             
peake.draw <- peake.rstanarm4 |> gather_draws(`.Intercept.*|.*AREA.*`, regex = TRUE)
peake.draw
# A tibble: 4,800 × 5
# Groups:   .variable [2]
   .chain .iteration .draw .variable   .value
    <int>      <int> <int> <chr>        <dbl>
 1      1          1     1 (Intercept) -1.33 
 2      1          2     2 (Intercept) -1.54 
 3      1          3     3 (Intercept) -1.64 
 4      1          4     4 (Intercept) -1.05 
 5      1          5     5 (Intercept) -1.05 
 6      1          6     6 (Intercept) -1.46 
 7      1          7     7 (Intercept) -0.206
 8      1          8     8 (Intercept) -1.07 
 9      1          9     9 (Intercept) -1.29 
10      1         10    10 (Intercept) -0.655
# ℹ 4,790 more rows

We can then summarise this

peake.draw |> median_hdci()
# A tibble: 2 × 7
  .variable   .value .lower .upper .width .point .interval
  <chr>        <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 (Intercept) -1.17  -2.58   0.239   0.95 median hdci     
2 log(AREA)    0.827  0.675  1.01    0.95 median hdci     
peake.rstanarm4 |>
  gather_draws(`.Intercept.*|.*AREA.*`, regex = TRUE) |>
  ggplot() +
  stat_halfeye(aes(x = .value, y = .variable)) +
  facet_wrap(~.variable, scales = "free")

We could alternatively express the parameters on the response scale.

peake.rstanarm4 |>
  gather_draws(`.Intercept.*|.*AREA.*`, regex = TRUE) |>
  group_by(.variable) |>
  mutate(.value = exp(.value)) |>
  median_hdci()
# A tibble: 2 × 7
  .variable   .value .lower .upper .width .point .interval
  <chr>        <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 (Intercept)  0.309 0.0262   1.00   0.95 median hdci     
2 log(AREA)    2.29  1.96     2.73   0.95 median hdci     

Conclusions:

  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.17. This is the median of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.31. When the mussel clump is 0, we expect to find 0.31 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.83 (mean) with a standard error of 0.68. The 95% credibility intervals indicate that we are 90% confident that the slope is between 1.01 and 0.95 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.29. This represents a 129% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.rstanarm4 |> plot(plotfun = "mcmc_intervals")

This is purely a graphical depiction on the posteriors.

peake.rstanarm4 |> tidy_draws()
# A tibble: 2,400 × 12
   .chain .iteration .draw `(Intercept)` `log(AREA)` reciprocal_dispersion
    <int>      <int> <int>         <dbl>       <dbl>                 <dbl>
 1      1          1     1        -1.33        0.857                  3.47
 2      1          2     2        -1.54        0.867                  4.63
 3      1          3     3        -1.64        0.890                  7.15
 4      1          4     4        -1.05        0.797                  2.97
 5      1          5     5        -1.05        0.830                  4.18
 6      1          6     6        -1.46        0.855                  6.55
 7      1          7     7        -0.206       0.720                  5.24
 8      1          8     8        -1.07        0.811                  3.59
 9      1          9     9        -1.29        0.843                  4.28
10      1         10    10        -0.655       0.758                  4.20
# ℹ 2,390 more rows
# ℹ 6 more variables: accept_stat__ <dbl>, stepsize__ <dbl>, treedepth__ <dbl>,
#   n_leapfrog__ <dbl>, divergent__ <dbl>, energy__ <dbl>
peake.rstanarm4 |> spread_draws(`.Intercept.*|.*AREA.*`, regex = TRUE)
# A tibble: 2,400 × 5
   .chain .iteration .draw `(Intercept)` `log(AREA)`
    <int>      <int> <int>         <dbl>       <dbl>
 1      1          1     1        -1.33        0.857
 2      1          2     2        -1.54        0.867
 3      1          3     3        -1.64        0.890
 4      1          4     4        -1.05        0.797
 5      1          5     5        -1.05        0.830
 6      1          6     6        -1.46        0.855
 7      1          7     7        -0.206       0.720
 8      1          8     8        -1.07        0.811
 9      1          9     9        -1.29        0.843
10      1         10    10        -0.655       0.758
# ℹ 2,390 more rows
## summarised
peake.rstanarm4 |>
  spread_draws(`.Intercept.*|.*AREA.*`, regex = TRUE) |>
  summarise_draws(
    "median",
    ~ HDInterval::hdi(.x),
    "rhat",
    "ess_bulk"
  )
# A tibble: 2 × 6
  variable    median  lower upper  rhat ess_bulk
  <chr>        <dbl>  <dbl> <dbl> <dbl>    <dbl>
1 (Intercept) -1.17  -2.63  0.181  1.00    1972.
2 log(AREA)    0.827  0.675 1.01   1.00    2017.
## summarised on fractional scale
peake.rstanarm4 |>
  spread_draws(`.Intercept.*|.*AREA.*`, regex = TRUE) |>
  mutate(across(everything(), exp)) |>
  summarise_draws(
    "median",
    ~ HDInterval::hdi(.x),
    "rhat",
    "ess_bulk"
  )
# A tibble: 2 × 6
  variable    median  lower upper  rhat ess_bulk
  <chr>        <dbl>  <dbl> <dbl> <dbl>    <dbl>
1 (Intercept)  0.309 0.0262  1.00  1.00    1972.
2 log(AREA)    2.29  1.95    2.71  1.00    2017.
peake.rstanarm4 |>
  posterior_samples() |>
  as_tibble()
Warning: Method 'posterior_samples' is deprecated. Please see ?as_draws for
recommended alternatives.
# A tibble: 2,400 × 3
   `(Intercept)` `log(AREA)` reciprocal_dispersion
           <dbl>       <dbl>                 <dbl>
 1        -1.33        0.857                  3.47
 2        -1.54        0.867                  4.63
 3        -1.64        0.890                  7.15
 4        -1.05        0.797                  2.97
 5        -1.05        0.830                  4.18
 6        -1.46        0.855                  6.55
 7        -0.206       0.720                  5.24
 8        -1.07        0.811                  3.59
 9        -1.29        0.843                  4.28
10        -0.655       0.758                  4.20
# ℹ 2,390 more rows

Unfortunately, \(R^2\) calculations for models other than Gaussian and Binomial have not yet been implemented for rstanarm models yet.

## peake.rstanarm4 |> bayes_R2() |> median_hdci

The summary() method generates simple summaries (mean, standard deviation as well as 10, 50 and 90 percentiles).

summary(peake.brm4)
 Family: negbinomial 
  Links: mu = log; shape = identity 
Formula: INDIV ~ log(AREA) 
   Data: peake (Number of observations: 25) 
  Draws: 3 chains, each with iter = 5000; warmup = 1000; thin = 5;
         total post-warmup draws = 2400

Regression Coefficients:
          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept    -1.13      0.71    -2.48     0.30 1.00     2342     2217
logAREA       0.82      0.08     0.65     0.98 1.00     2302     2253

Further Distributional Parameters:
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
shape     5.00      1.39     2.68     8.04 1.00     2020     2291

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Conclusions:

  • in the Model Info, we are informed that the total MCMC posterior sample size is 2400 and that there were 19 raw observations.
  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.13. This is the mean of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.32. When the mussel clump is 0, we expect to find 0.32 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.82 (mean) or 0.98 (median) with a standard deviation of 0.08. The 90% credibility intervals indicate that we are 90% confident that the slope is between 0.82 and 1 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.27. This represents a 127% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.brm4 |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 9 × 8
  variable           median     lower    upper  rhat length ess_bulk ess_tail
  <chr>               <dbl>     <dbl>    <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 b_Intercept       -1.14     -2.43      0.338 0.999   2400    2342.    2217.
2 b_logAREA          0.822     0.648     0.974 0.999   2400    2302.    2253.
3 shape              4.85      2.66      8.01  1.00    2400    2019.    2291.
4 Intercept          5.73      5.55      5.93  0.999   2400    2344.    2239.
5 prior_Intercept    6.03      3.35      9.08  1.000   2400    2217.    1989.
6 prior_b            0.0163   -1.90      1.99  1.00    2400    2611.    2408.
7 prior_shape        1.68      0.0338    4.69  1.00    2400    2550.    2405.
8 lprior            -5.87     -8.33     -4.16  1.00    2400    2012.    2277.
9 lp__            -160.     -162.     -158.    1.00    2400    2244.    2455.

We can also alter the CI level.

peake.brm4 |>
  summarise_draws(
    median,
    ~ HDInterval::hdi(.x, credMass = 0.9),
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 9 × 8
  variable           median    lower     upper  rhat length ess_bulk ess_tail
  <chr>               <dbl>    <dbl>     <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 b_Intercept       -1.14     -2.27     0.0234 0.999   2400    2342.    2217.
2 b_logAREA          0.822     0.683    0.952  0.999   2400    2302.    2253.
3 shape              4.85      2.88     7.35   1.00    2400    2019.    2291.
4 Intercept          5.73      5.59     5.90   0.999   2400    2344.    2239.
5 prior_Intercept    6.03      3.59     8.44   1.000   2400    2217.    1989.
6 prior_b            0.0163   -1.54     1.79   1.00    2400    2611.    2408.
7 prior_shape        1.68      0.132    3.89   1.00    2400    2550.    2405.
8 lprior            -5.87     -7.81    -4.31   1.00    2400    2012.    2277.
9 lp__            -160.     -162.    -158.     1.00    2400    2244.    2455.

To narrow down to just the parameters of interest, see the code under the tidy_draws tab.

tidyMCMC(peake.brm4$fit, estimate.method = "median", conf.int = TRUE, conf.method = "HPDinterval", rhat = TRUE, ess = TRUE)
# A tibble: 8 × 7
  term            estimate std.error conf.low conf.high  rhat   ess
  <chr>              <dbl>     <dbl>    <dbl>     <dbl> <dbl> <int>
1 b_Intercept      -1.13      0.712   -2.43       0.338 0.999  2303
2 b_logAREA         0.821     0.0838   0.648      0.974 0.999  2267
3 shape             5.00      1.39     2.66       8.01  1.00   1987
4 Intercept         5.73      0.0963   5.55       5.93  0.999  2351
5 prior_Intercept   6.03      1.48     3.35       9.08  0.999  2210
6 prior_b           0.0341    1.02    -1.90       1.99  1.00   2599
7 prior_shape       2.00      1.39     0.0338     4.69  1.00   2496
8 lprior           -6.03      1.11    -8.33      -4.16  1.00   1980

Conclusions:

  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.13. This is the median of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.32. When the mussel clump is 0, we expect to find 0.32 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.82 (mean) with a standard error of 0.08. The 95% credibility intervals indicate that we are 90% confident that the slope is between 0.65 and 0.97 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.27. This represents a 127% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.brm4 |> as_draws_df()
# A draws_df: 800 iterations, 3 chains, and 9 variables
   b_Intercept b_logAREA shape Intercept prior_Intercept prior_b prior_shape
1        -1.26      0.84   3.8       5.7             5.2   -1.07        2.76
2        -1.21      0.83   3.0       5.8             5.3   -0.29        1.45
3        -2.04      0.92   3.4       5.7             9.4   -0.24        2.20
4        -0.62      0.76   5.0       5.7             5.0   -1.13        0.90
5        -1.27      0.85   4.6       5.8             5.1    1.83        3.03
6        -1.59      0.89   3.6       5.9             4.5   -1.50        1.77
7        -1.21      0.84   4.9       5.8             4.9    0.91        0.79
8        -2.00      0.94   3.1       5.9             7.8    1.54        1.61
9        -1.44      0.88   5.0       5.9             5.3   -2.04        1.23
10       -1.05      0.81   6.2       5.8             4.2   -1.62        0.16
   lprior
1    -5.1
2    -4.5
3    -4.9
4    -5.9
5    -5.6
6    -4.9
7    -5.9
8    -4.7
9    -6.0
10   -7.0
# ... with 2390 more draws, and 1 more variables
# ... hidden reserved variables {'.chain', '.iteration', '.draw'}
## summarised
peake.brm4 |>
  as_draws_df() |>
  summarise_draws(
    median,
    ~ HDInterval::hdi(.x),
    length,
    rhat,
    ess_bulk, ess_tail
  )
# A tibble: 9 × 8
  variable           median     lower    upper length  rhat ess_bulk ess_tail
  <chr>               <dbl>     <dbl>    <dbl>  <dbl> <dbl>    <dbl>    <dbl>
1 b_Intercept       -1.14     -2.43      0.338   2400 0.999    2342.    2217.
2 b_logAREA          0.822     0.648     0.974   2400 0.999    2302.    2253.
3 shape              4.85      2.66      8.01    2400 1.00     2019.    2291.
4 Intercept          5.73      5.55      5.93    2400 0.999    2344.    2239.
5 prior_Intercept    6.03      3.35      9.08    2400 1.000    2217.    1989.
6 prior_b            0.0163   -1.90      1.99    2400 1.00     2611.    2408.
7 prior_shape        1.68      0.0338    4.69    2400 1.00     2550.    2405.
8 lprior            -5.87     -8.33     -4.16    2400 1.00     2012.    2277.
9 lp__            -160.     -162.     -158.      2400 1.00     2244.    2455.
## summarised on fractional scale
peake.brm4 |>
  as_draws_df() |>
  dplyr::select(starts_with("b_")) |>
  ## mutate(across(everything(), exp)) |>
  exp() |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat,
    length,
    ess_bulk, ess_tail
  )
Warning: Dropping 'draws_df' class as required metadata was removed.
# A tibble: 2 × 8
  variable    median  lower upper  rhat length ess_bulk ess_tail
  <chr>        <dbl>  <dbl> <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 b_Intercept  0.321 0.0315  1.05 1.000   2400    2321.    2210.
2 b_logAREA    2.28  1.91    2.65 1.000   2400    2281.    2246.

Return the draws (samples) for each parameter in wide format

peake.brm4 |> tidy_draws()
# A tibble: 2,400 × 18
   .chain .iteration .draw b_Intercept b_logAREA shape Intercept prior_Intercept
    <int>      <int> <int>       <dbl>     <dbl> <dbl>     <dbl>           <dbl>
 1      1          1     1      -1.26      0.837  3.81      5.73            5.20
 2      1          2     2      -1.21      0.834  2.98      5.76            5.34
 3      1          3     3      -2.04      0.923  3.39      5.67            9.44
 4      1          4     4      -0.620     0.756  5.00      5.70            4.96
 5      1          5     5      -1.27      0.846  4.55      5.79            5.12
 6      1          6     6      -1.59      0.892  3.56      5.86            4.50
 7      1          7     7      -1.21      0.836  4.94      5.77            4.93
 8      1          8     8      -2.00      0.940  3.10      5.85            7.80
 9      1          9     9      -1.44      0.875  4.95      5.87            5.30
10      1         10    10      -1.05      0.814  6.23      5.75            4.23
# ℹ 2,390 more rows
# ℹ 10 more variables: prior_b <dbl>, prior_shape <dbl>, lprior <dbl>,
#   lp__ <dbl>, accept_stat__ <dbl>, stepsize__ <dbl>, treedepth__ <dbl>,
#   n_leapfrog__ <dbl>, divergent__ <dbl>, energy__ <dbl>
peake.brm4 |> get_variables()
 [1] "b_Intercept"     "b_logAREA"       "shape"           "Intercept"      
 [5] "prior_Intercept" "prior_b"         "prior_shape"     "lprior"         
 [9] "lp__"            "accept_stat__"   "stepsize__"      "treedepth__"    
[13] "n_leapfrog__"    "divergent__"     "energy__"       
peake.brm4$fit |>
  tidy_draws() |>
  dplyr::select(matches("^b_.*")) |>
  exp() |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 2 × 8
  variable    median  lower upper  rhat length ess_bulk ess_tail
  <chr>        <dbl>  <dbl> <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 b_Intercept  0.321 0.0315  1.05 1.000   2400    2321.    2210.
2 b_logAREA    2.28  1.91    2.65 1.000   2400    2281.    2246.

The above can be useful for exploring the full posteriors of all the parameters of performing specific calculations using the posteriors, summarising the parameters takes a few more steps.

peake.brm4 |>
  tidy_draws() |>
  exp() |>
  gather_variables() |>
  median_hdci()
# A tibble: 15 × 7
   .variable         .value   .lower   .upper .width .point .interval
   <chr>              <dbl>    <dbl>    <dbl>  <dbl> <chr>  <chr>    
 1 accept_stat__   2.62e+ 0 2.20e+ 0 2.72e+ 0   0.95 median hdci     
 2 b_Intercept     3.21e- 1 4.45e- 2 1.10e+ 0   0.95 median hdci     
 3 b_logAREA       2.28e+ 0 1.91e+ 0 2.65e+ 0   0.95 median hdci     
 4 divergent__     1   e+ 0 1   e+ 0 1   e+ 0   0.95 median hdci     
 5 energy__        8.81e+69 6.34e+68 3.31e+71   0.95 median hdci     
 6 Intercept       3.09e+ 2 2.54e+ 2 3.71e+ 2   0.95 median hdci     
 7 lp__            5.27e-70 2.79e-74 1.59e-69   0.95 median hdci     
 8 lprior          2.81e- 3 2.74e- 5 1.10e- 2   0.95 median hdci     
 9 n_leapfrog__    1.10e+ 3 2.01e+ 1 1.10e+ 3   0.95 median hdci     
10 prior_b         1.02e+ 0 4.53e- 2 5.53e+ 0   0.95 median hdci     
11 prior_Intercept 4.17e+ 2 3.39e+ 0 4.65e+ 3   0.95 median hdci     
12 prior_shape     5.38e+ 0 1.03e+ 0 1.09e+ 2   0.95 median hdci     
13 shape           1.28e+ 2 3.83e+ 0 1.91e+ 3   0.95 median hdci     
14 stepsize__      1.98e+ 0 1.90e+ 0 2.03e+ 0   0.95 median hdci     
15 treedepth__     7.39e+ 0 7.39e+ 0 2.01e+ 1   0.95 median hdci     

The gather_draws on the other hand, conveniently combines tidy_draws and gather_variables together in a single command. Furthermore, it returns all of the variables. The spread_draws function allows users to target specific variables (either by naming them in full or via regexp).

Due to the presence of a log transform in the predictor, it is better to use the regex version.

peake.brm4 |> get_variables()
 [1] "b_Intercept"     "b_logAREA"       "shape"           "Intercept"      
 [5] "prior_Intercept" "prior_b"         "prior_shape"     "lprior"         
 [9] "lp__"            "accept_stat__"   "stepsize__"      "treedepth__"    
[13] "n_leapfrog__"    "divergent__"     "energy__"       
peake.draw <- peake.brm4 |> gather_draws(`b_.*`, regex = TRUE)
peake.draw
# A tibble: 4,800 × 5
# Groups:   .variable [2]
   .chain .iteration .draw .variable   .value
    <int>      <int> <int> <chr>        <dbl>
 1      1          1     1 b_Intercept -1.26 
 2      1          2     2 b_Intercept -1.21 
 3      1          3     3 b_Intercept -2.04 
 4      1          4     4 b_Intercept -0.620
 5      1          5     5 b_Intercept -1.27 
 6      1          6     6 b_Intercept -1.59 
 7      1          7     7 b_Intercept -1.21 
 8      1          8     8 b_Intercept -2.00 
 9      1          9     9 b_Intercept -1.44 
10      1         10    10 b_Intercept -1.05 
# ℹ 4,790 more rows

We can then summarise this

peake.draw |> median_hdci()
# A tibble: 2 × 7
  .variable   .value .lower .upper .width .point .interval
  <chr>        <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 b_Intercept -1.14  -2.39   0.377   0.95 median hdci     
2 b_logAREA    0.822  0.650  0.978   0.95 median hdci     
peake.brm4 |>
  gather_draws(`b_.*`, regex = TRUE) |>
  mutate(.value = exp(.value)) |>
  summarise_draws(
    median,
    HDInterval::hdi,
    rhat, length, ess_bulk, ess_tail
  )
# A tibble: 2 × 9
# Groups:   .variable [2]
  .variable   variable median  lower upper  rhat length ess_bulk ess_tail
  <chr>       <chr>     <dbl>  <dbl> <dbl> <dbl>  <dbl>    <dbl>    <dbl>
1 b_Intercept .value    0.321 0.0315  1.05 0.999   2400    2342.    2217.
2 b_logAREA   .value    2.28  1.91    2.65 0.999   2400    2302.    2253.
peake.brm4 |>
  gather_draws(`b_.*`, regex = TRUE) |>
  ggplot() +
  stat_halfeye(aes(x = .value, y = .variable)) +
  facet_wrap(~.variable, scales = "free")

We could alternatively express the parameters on the response scale.

peake.brm4 |>
  gather_draws(`.Intercept.*|.*AREA.*`, regex = TRUE) |>
  group_by(.variable) |>
  mutate(.value = exp(.value)) |>
  median_hdci()
# A tibble: 1 × 7
  .variable .value .lower .upper .width .point .interval
  <chr>      <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1 b_logAREA   2.28   1.91   2.65   0.95 median hdci     

Conclusions:

  • the estimated intercept (expected number of individuals for a mussel clump of log-transformed area = 0) is -1.14. This is the median of the posterior distribution for this parameter. If we back-transform this to the response scale, this becomes 0.32. When the mussel clump is 0, we expect to find 0.32 individuals in the mussel clump (which obviously does not exist if it has a log-transformed area of 0!).
  • the estimated slope (rate at which the number of individuals changes per 1 unit change in log-transformed area), is 0.82 (mean) with a standard error of 0.65. The 95% credibility intervals indicate that we are 90% confident that the slope is between 0.98 and 0.95 - e.g. there is a significant positive trend. When back-transformed onto the reponse scale, we see that for a one unit increase in log-transformed mussel clump area, the number of individuals increases by a factor of 2.28. This represents a 128% increase per 1 unit change in log-transformed mussel clump area.
  • Rhat and number of effective samples for each parameter are also provided as MCMC diagnostics and all look good.
peake.brm4 |> plot(plotfun = "mcmc_intervals")

peake.brm4 |> spread_draws(`b_.*`, regex = TRUE)
# A tibble: 2,400 × 5
   .chain .iteration .draw b_Intercept b_logAREA
    <int>      <int> <int>       <dbl>     <dbl>
 1      1          1     1      -1.26      0.837
 2      1          2     2      -1.21      0.834
 3      1          3     3      -2.04      0.923
 4      1          4     4      -0.620     0.756
 5      1          5     5      -1.27      0.846
 6      1          6     6      -1.59      0.892
 7      1          7     7      -1.21      0.836
 8      1          8     8      -2.00      0.940
 9      1          9     9      -1.44      0.875
10      1         10    10      -1.05      0.814
# ℹ 2,390 more rows
## summarised
peake.brm4 |>
  as_draws_df() |>
  dplyr::select(starts_with("b_")) |>
  summarise_draws(
    "median",
    ~ HDInterval::hdi(.x),
    "rhat",
    "ess_bulk"
  )
Warning: Dropping 'draws_df' class as required metadata was removed.
# A tibble: 2 × 6
  variable    median  lower upper  rhat ess_bulk
  <chr>        <dbl>  <dbl> <dbl> <dbl>    <dbl>
1 b_Intercept -1.14  -2.43  0.338 1.000    2322.
2 b_logAREA    0.822  0.648 0.974 1.000    2281.
## summarised on fractional scale
peake.brm4 |>
  as_draws_df() |>
  dplyr::select(starts_with("b_")) |>
  mutate(across(everything(), exp)) |>
  summarise_draws(
    "median",
    ~ HDInterval::hdi(.x),
    "rhat",
    "ess_bulk"
  )
Warning: Dropping 'draws_df' class as required metadata was removed.
# A tibble: 2 × 6
  variable    median  lower upper  rhat ess_bulk
  <chr>        <dbl>  <dbl> <dbl> <dbl>    <dbl>
1 b_Intercept  0.321 0.0315  1.05 1.000    2322.
2 b_logAREA    2.28  1.91    2.65 1.000    2281.
peake.brm4 |>
  posterior_samples() |>
  as_tibble()
Warning: Method 'posterior_samples' is deprecated. Please see ?as_draws for
recommended alternatives.
# A tibble: 2,400 × 9
   b_Intercept b_logAREA shape Intercept prior_Intercept prior_b prior_shape
         <dbl>     <dbl> <dbl>     <dbl>           <dbl>   <dbl>       <dbl>
 1      -1.26      0.837  3.81      5.73            5.20  -1.07        2.76 
 2      -1.21      0.834  2.98      5.76            5.34  -0.289       1.45 
 3      -2.04      0.923  3.39      5.67            9.44  -0.240       2.20 
 4      -0.620     0.756  5.00      5.70            4.96  -1.13        0.901
 5      -1.27      0.846  4.55      5.79            5.12   1.83        3.03 
 6      -1.59      0.892  3.56      5.86            4.50  -1.50        1.77 
 7      -1.21      0.836  4.94      5.77            4.93   0.910       0.792
 8      -2.00      0.940  3.10      5.85            7.80   1.54        1.61 
 9      -1.44      0.875  4.95      5.87            5.30  -2.04        1.23 
10      -1.05      0.814  6.23      5.75            4.23  -1.62        0.157
# ℹ 2,390 more rows
# ℹ 2 more variables: lprior <dbl>, lp__ <dbl>
peake.brm4 |>
  bayes_R2(summary = FALSE) |>
  median_hdci()
          y      ymin      ymax .width .point .interval
1 0.7334672 0.6697274 0.7529402   0.95 median      hdci
peake.brm4 |> modelsummary(
  statistic = c("conf.low", "conf.high"),
  shape = term ~ statistic
)
Warning: 
`modelsummary` uses the `performance` package to extract goodness-of-fit
statistics from models of this class. You can specify the statistics you wish
to compute by supplying a `metrics` argument to `modelsummary`, which will then
push it forward to `performance`. Acceptable values are: "all", "common",
"none", or a character vector of metrics names. For example: `modelsummary(mod,
metrics = c("RMSE", "R2")` Note that some metrics are computationally
expensive. See `?performance::performance` for details.
 This warning appears once per session.
(1)
Est. 2.5 % 97.5 %
b_Intercept -1.137 -2.483 0.301
b_logAREA 0.822 0.651 0.979
Num.Obs. 25
R2 0.733
ELPD -156.5
ELPD s.e. 5.4
LOOIC 313.1
LOOIC s.e. 10.8
WAIC 313.0
RMSE 250.80
peake.brm4 |> modelsummary(
  statistic = c("conf.low", "conf.high"),
  shape = term ~ statistic,
  exponentiate = TRUE
)
(1)
Est. 2.5 % 97.5 %
b_Intercept 0.321 0.083 1.351
b_logAREA 2.275 1.918 2.662
Num.Obs. 25
R2 0.733
ELPD -156.5
ELPD s.e. 5.4
LOOIC 313.1
LOOIC s.e. 10.8
WAIC 313.0
RMSE 250.84
modelsummary(list("Raw" = peake.brm4, "Exponentiated" = peake.brm4),
  statistic = c("conf.low", "conf.high"),
  shape = term ~ statistic,
  exponentiate = c(FALSE, TRUE)
)
Raw Exponentiated
Est. 2.5 % 97.5 % Est. 2.5 % 97.5 %
b_Intercept -1.137 -2.483 0.301 0.321 0.083 1.351
b_logAREA 0.822 0.651 0.979 2.275 1.918 2.662
Num.Obs. 25 25
R2 0.733 0.733
ELPD -156.5 -156.5
ELPD s.e. 5.4 5.4
LOOIC 313.1 313.1
LOOIC s.e. 10.8 10.8
WAIC 313.0 313.0
RMSE 250.12 251.33
peake.brm4 |> modelplot()

peake.brm4 |> modelplot(exponentiate = TRUE)

11 Hypothesis testing

Unfortunately, the inline log-transformation of the predictor interfers with hypothesis()’s ability to find the slope. We can get around this by renaming before calling hypothesis().

peake.rstanarm4 |>
  as.data.frame() |>
  rename(lAREA = `log(AREA)`) |>
  hypothesis("lAREA>0")
Hypothesis Tests for class :
   Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob Star
1 (lAREA) > 0     0.83      0.08     0.69     0.96        Inf         1    *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.

Conclusions:

  • the probability of a positive effect of mussel clump area on number of individuals is 1. That is, there is, we are 100% confident that there is an effect.
  • the evidence ratio is normally the ratio of the number of cases that satisty the hypothesis to the number of cases that do not. Since the number of cases that do not satisfy the hypothesis is 0, the evidence ratio is Inf (since division by 0)

Alternatively…

peake.rstanarm4 |>
  tidy_draws() |>
  summarise(P = mean(`log(AREA)` > 0))
# A tibble: 1 × 1
      P
  <dbl>
1     1

Conclusions:

  • the probability of a positive effect of mussel clump area on number of individuals is 1. That is, there is, we are 100% confident that there is an effect.
newdata <- list(AREA = c(5000, 10000))
## fractional scale
peake.rstanarm4 |>
  emmeans(~AREA, at = newdata, type = "response") |>
  pairs(reverse = TRUE)
 contrast             ratio lower.HPD upper.HPD
 AREA10000 / AREA5000  1.77       1.6      2.01

Point estimate displayed: median 
Results are back-transformed from the log scale 
HPD interval probability: 0.95 
## absolute scale
peake.rstanarm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  pairs(reverse = TRUE)
 contrast             estimate lower.HPD upper.HPD
 AREA10000 - AREA5000      273       189       378

Point estimate displayed: median 
HPD interval probability: 0.95 

Conclusions:

  • the change in expected number of individuals associated with an increase in mussel clump area from 5,000 to 10,000 is 0.57.

If we want to derive other properties, such as the percentage change, then we use tidy_draws() and then simple tidyverse spreadsheet operation.

peake.mcmc <- peake.rstanarm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  tidy_draws() |>
  rename_with(~ str_replace(., "AREA ", "p")) |>
  mutate(
    Eff = p10000 - p5000,
    PEff = 100 * Eff / p5000
  )
peake.mcmc |> head()
# A tibble: 6 × 7
  .chain .iteration .draw p5000 p10000   Eff  PEff
   <int>      <int> <int> <dbl>  <dbl> <dbl> <dbl>
1      1          1     1  392.   710.  318.  81.2
2      1          2     2  347.   632.  286.  82.4
3      1          3     3  379.   702.  323.  85.3
4      1          4     4  311.   541.  229.  73.7
5      1          5     5  410.   729.  319.  77.8
6      1          6     6  336.   607.  271.  80.8

Now we can calculate the medians and HPD intervals of each column (and ignore the .chain, .iteration and .draw).

peake.mcmc |> tidyMCMC(
  estimate.method = "median",
  conf.int = TRUE, conf.method = "HPDinterval"
)
# A tibble: 7 × 5
  term       estimate std.error conf.low conf.high
  <chr>         <dbl>     <dbl>    <dbl>     <dbl>
1 .chain          2       0.817      1          3 
2 .iteration    400.    231.         1        761 
3 .draw        1200.    693.         1       2281 
4 p5000         356.     35.2      287.       422.
5 p10000        633.     77.7      497.       789.
6 Eff           277.     49.9      189.       378.
7 PEff           77.6    10.4       59.7      101.

Alternatively, we could use median_hdci

peake.mcmc |> median_hdci(PEff)
# A tibble: 1 × 6
   PEff .lower .upper .width .point .interval
  <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1  77.4   59.7   101.   0.95 median hdci     

Conclusions:

  • the percentage change in expected number of individuals associated with an increase in mussel clump area from 5,000 to 10,000 is 77.42% with a 95% credibility interval of 59.7 100.76

To get the probability that the effect is greater than a 50% increase.

peake.mcmc |> summarise(P = mean(PEff > 50))
# A tibble: 1 × 1
      P
  <dbl>
1 0.997

Conclusions:

  • the probability that the number of individuals will increase by more than 50% following an increase in mussel clump area from 5,000 to 10,000 is

Finally, we could alternatively use hypothesis(). Note that when we do so, the estimate is the difference between the effect and the hypothesised effect (50%).

peake.mcmc |> hypothesis("PEff>50")
Hypothesis Tests for class :
       Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob
1 (PEff)-(50) > 0     27.6     10.42    10.91    44.94        299         1
  Star
1    *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.
## Note, the P value is on absolute difference
newdata <- list(AREA = c(5000, 10000))
peake.rstanarm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  pairs(reverse = TRUE) |>
  tidy_draws() |>
  summarise(across(contains("contrast"),
    list(
      P = ~ mean(. > 50),
      HDCI = ~ median_hdci(.)
    ),
    .names = c("{.fn}")
  )) |>
  tidyr::unpack(HDCI)
# A tibble: 1 × 7
      P     y  ymin  ymax .width .point .interval
  <dbl> <dbl> <dbl> <dbl>  <dbl> <chr>  <chr>    
1     1  273.  189.  378.   0.95 median hdci     

We can also express this as a percentage change

newdata <- list(AREA = c(5000, 10000))
## Simple
peake.rstanarm4 |>
  emmeans(~AREA, at = newdata) |>
  pairs(reverse = TRUE) |>
  regrid()
 contrast           ratio lower.HPD upper.HPD
 AREA10000/AREA5000  1.77       1.6      2.01

Point estimate displayed: median 
HPD interval probability: 0.95 
## More advanced (both P and percent change)
peake.mcmc <- peake.rstanarm4 |>
  emmeans(~AREA, at = newdata) |>
  pairs(reverse = TRUE) |>
  regrid() |>
  tidy_draws() |>
  mutate(across(contains("contrast"), ~ 100 * (. - 1)))

peake.mcmc |>
  summarise(across(contains("contrast"),
    list(
      P = ~ mean(. > 50),
      HDCI = ~ median_hdci(.)
    ),
    .names = c("{.fn}")
  )) |>
  tidyr::unpack(HDCI)
# A tibble: 1 × 7
      P     y  ymin  ymax .width .point .interval
  <dbl> <dbl> <dbl> <dbl>  <dbl> <chr>  <chr>    
1 0.997  77.4  59.7  101.   0.95 median hdci     
peake.rstanarm4 |>
  linpred_draws(newdata = as.data.frame(newdata)) |>
  mutate(.linpred = exp(.linpred)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.linpred),
    PEff = 100 * Eff / .linpred[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value  .lower  .upper .width .point .interval
  <chr>   <dbl>   <dbl>   <dbl>  <dbl> <chr>  <chr>    
1 Eff   273.    189.    378.      0.95 median hdci     
2 P       0.997   0.997   0.997   0.95 median hdci     
3 PEff   77.4    59.7   101.      0.95 median hdci     
## OR
peake.rstanarm4 |>
  epred_draws(newdata = as.data.frame(newdata)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.epred),
    PEff = 100 * Eff / .epred[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value  .lower  .upper .width .point .interval
  <chr>   <dbl>   <dbl>   <dbl>  <dbl> <chr>  <chr>    
1 Eff   273.    189.    378.      0.95 median hdci     
2 P       0.997   0.997   0.997   0.95 median hdci     
3 PEff   77.4    59.7   101.      0.95 median hdci     
## OR for prediction of individual values
peake.rstanarm4 |>
  predicted_draws(newdata = as.data.frame(newdata)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.prediction),
    PEff = 100 * Eff / .prediction[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value   .lower   .upper .width .point .interval
  <chr>   <dbl>    <dbl>    <dbl>  <dbl> <chr>  <chr>    
1 Eff   243     -361.    1073.      0.95 median hdci     
2 P       0.599    0.599    0.599   0.95 median hdci     
3 PEff   79.0    -79.4    504.      0.95 median hdci     
peake.brm4 |> hypothesis("logAREA>0")
Hypothesis Tests for class b:
     Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob Star
1 (logAREA) > 0     0.82      0.08     0.68     0.95        Inf         1    *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.

Conclusions:

  • the probability of a positive effect of mussel clump area on number of individuals is 1. That is, there is, we are 100% confident that there is an effect.
  • the evidence ratio is normally the ratio of the number of cases that satisty the hypothesis to the number of cases that do not. Since the number of cases that do not satisfy the hypothesis is 0, the evidence ratio is Inf (since division by 0)

Alternatively…

peake.brm4 |>
  tidy_draws() |>
  summarise(P = mean(b_logAREA > 0))
# A tibble: 1 × 1
      P
  <dbl>
1     1

Conclusions:

  • the probability of a positive effect of mussel clump area on number of individuals is 1. That is, there is, we are 100% confident that there is an effect.
newdata <- list(AREA = c(5000, 10000))
## fractional scale
peake.brm4 |>
  emmeans(~AREA, at = newdata, type = "response") |>
  pairs(reverse = TRUE)
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
 contrast             ratio lower.HPD upper.HPD
 AREA10000 / AREA5000  1.77      1.57      1.96

Point estimate displayed: median 
Results are back-transformed from the log scale 
HPD interval probability: 0.95 
## absolute scale
peake.brm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  pairs(reverse = TRUE)
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
 contrast             estimate lower.HPD upper.HPD
 AREA10000 - AREA5000      271       187       370

Point estimate displayed: median 
HPD interval probability: 0.95 
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.

Conclusions:

  • the change in expected number of individuals associated with an increase in mussel clump area from 5,000 to 10,000 is 0.57.

If we want to derive other properties, such as the percentage change, then we use tidy_draws() and then simple tidyverse spreadsheet operation.

peake.mcmc <- peake.brm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  tidy_draws() |>
  rename_with(~ str_replace(., "AREA ", "p")) |>
  mutate(
    Eff = p10000 - p5000,
    PEff = 100 * Eff / p5000
  )
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
peake.mcmc |> head()
# A tibble: 6 × 7
  .chain .iteration .draw p5000 p10000   Eff  PEff
   <int>      <int> <int> <dbl>  <dbl> <dbl> <dbl>
1      1          1     1  353.   631.  278.  78.6
2      1          2     2  363.   646.  284.  78.2
3      1          3     3  338.   640.  303.  89.7
4      1          4     4  338.   571.  233.  68.9
5      1          5     5  377.   678.  301.  79.7
6      1          6     6  405.   751.  346.  85.6

Now we can calculate the medians and HPD intervals of each column (and ignore the .chain, .iteration and .draw).

peake.mcmc |> tidyMCMC(
  estimate.method = "median",
  conf.int = TRUE, conf.method = "HPDinterval"
)
# A tibble: 7 × 5
  term       estimate std.error conf.low conf.high
  <chr>         <dbl>     <dbl>    <dbl>     <dbl>
1 .chain          2       0.817      1         3  
2 .iteration    400.    231.         1       761  
3 .draw        1200.    693.         1      2281  
4 p5000         355.     34.6      294.      429. 
5 p10000        629.     74.6      498.      787. 
6 Eff           274.     47.7      187.      370. 
7 PEff           77.0    10.3       56.7      96.4

Alternatively, we could use median_hdci

peake.mcmc |> median_hdci(PEff)
# A tibble: 1 × 6
   PEff .lower .upper .width .point .interval
  <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
1  76.8   56.7   96.4   0.95 median hdci     

Conclusions:

  • the percentage change in expected number of individuals associated with an increase in mussel clump area from 5,000 to 10,000 is 76.81% with a 95% credibility interval of 56.73 96.39

To get the probability that the effect is greater than a 50% increase.

peake.mcmc |> summarise(P = mean(PEff > 50))
# A tibble: 1 × 1
      P
  <dbl>
1 0.996

Conclusions:

  • the probability that the number of individuals will increase by more than 50% following an increase in mussel clump area from 5,000 to 10,000 is

Finally, we could alternatively use hypothesis(). Note that when we do so, the estimate is the difference between the effect and the hypothesised effect (50%).

peake.mcmc |> hypothesis("PEff>50")
Hypothesis Tests for class :
       Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob
1 (PEff)-(50) > 0    27.02     10.26    10.52    43.49     265.67         1
  Star
1    *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.
## Note, the P value is on absolute difference
newdata <- list(AREA = c(5000, 10000))
peake.brm4 |>
  emmeans(~AREA, at = newdata) |>
  regrid() |>
  pairs(reverse = TRUE) |>
  tidy_draws() |>
  summarise(across(contains("contrast"),
    list(
      P = ~ mean(. > 50),
      HDCI = ~ median_hdci(.)
    ),
    .names = c("{.fn}")
  )) |>
  tidyr::unpack(HDCI)
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
# A tibble: 1 × 7
      P     y  ymin  ymax .width .point .interval
  <dbl> <dbl> <dbl> <dbl>  <dbl> <chr>  <chr>    
1     1  271.  187.  370.   0.95 median hdci     

We can also express this as a percentage change

newdata <- list(AREA = c(5000, 10000))
## Simple
peake.brm4 |>
  emmeans(~AREA, at = newdata) |>
  pairs(reverse = TRUE) |>
  regrid()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
 contrast           ratio lower.HPD upper.HPD
 AREA10000/AREA5000  1.77      1.57      1.96

Point estimate displayed: median 
HPD interval probability: 0.95 
## More advanced (both P and percent change)
peake.mcmc <- peake.brm4 |>
  emmeans(~AREA, at = newdata) |>
  pairs(reverse = TRUE) |>
  regrid() |>
  tidy_draws() |>
  mutate(across(contains("contrast"), ~ 100 * (. - 1)))
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
peake.mcmc |>
  summarise(across(contains("contrast"),
    list(
      P = ~ mean(. > 50),
      HDCI = ~ median_hdci(.)
    ),
    .names = c("{.fn}")
  )) |>
  tidyr::unpack(HDCI)
# A tibble: 1 × 7
      P     y  ymin  ymax .width .point .interval
  <dbl> <dbl> <dbl> <dbl>  <dbl> <chr>  <chr>    
1 0.996  76.8  56.7  96.4   0.95 median hdci     
peake.brm4 |>
  linpred_draws(newdata = as.data.frame(newdata)) |>
  mutate(.linpred = exp(.linpred)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.linpred),
    PEff = 100 * Eff / .linpred[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value  .lower  .upper .width .point .interval
  <chr>   <dbl>   <dbl>   <dbl>  <dbl> <chr>  <chr>    
1 Eff   271.    187.    370.      0.95 median hdci     
2 P       0.996   0.996   0.996   0.95 median hdci     
3 PEff   76.8    56.7    96.4     0.95 median hdci     
## OR
peake.brm4 |>
  epred_draws(newdata = as.data.frame(newdata)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.epred),
    PEff = 100 * Eff / .epred[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value  .lower  .upper .width .point .interval
  <chr>   <dbl>   <dbl>   <dbl>  <dbl> <chr>  <chr>    
1 Eff   271.    187.    370.      0.95 median hdci     
2 P       0.996   0.996   0.996   0.95 median hdci     
3 PEff   76.8    56.7    96.4     0.95 median hdci     
## OR for prediction of individual values
peake.brm4 |>
  predicted_draws(newdata = as.data.frame(newdata)) |>
  ungroup() |>
  group_by(.draw) |>
  summarise(
    Eff = diff(.prediction),
    PEff = 100 * Eff / .prediction[1]
  ) |>
  ungroup() |>
  mutate(P = mean(PEff > 50)) |>
  pivot_longer(cols = -.draw) |>
  group_by(name) |>
  median_hdci()
# A tibble: 3 × 7
  name    value   .lower   .upper .width .point .interval
  <chr>   <dbl>    <dbl>    <dbl>  <dbl> <chr>  <chr>    
1 Eff   249     -342.    1000.      0.95 median hdci     
2 P       0.593    0.593    0.593   0.95 median hdci     
3 PEff   76.6    -81.2    475.      0.95 median hdci     

12 Summary figure

## Using emmeans
peake.grid <- with(peake, list(AREA = seq(min(AREA), max(AREA), len = 100)))

newdata <- emmeans(peake.rstanarm4, ~AREA, at = peake.grid, type = "response") |> as.data.frame()
head(newdata)
      AREA      prob lower.HPD upper.HPD
  462.2500  49.26863  31.89159  72.82926
  731.7629  72.07244  49.43973  99.65874
 1001.2759  93.49842  66.61303 123.79848
 1270.7888 113.85418  85.48012 148.13290
 1540.3017 133.46134 101.07017 167.96983
 1809.8146 152.70935 117.74162 189.29512

Point estimate displayed: median 
Results are back-transformed from the log scale 
HPD interval probability: 0.95 
ggplot(newdata, aes(y = prob, x = AREA)) +
  geom_point(data = peake, aes(y = INDIV)) +
  geom_line() +
  geom_ribbon(aes(ymin = lower.HPD, ymax = upper.HPD), fill = "blue", alpha = 0.3) +
  scale_y_continuous("Individuals") +
  scale_x_continuous("Mussel clump area") +
  theme_classic()

## spaghetti plot
newdata <- emmeans(peake.rstanarm4, ~AREA, at = peake.grid) |>
  regrid() |>
  gather_emmeans_draws()
newdata |> head()
# A tibble: 6 × 5
# Groups:   AREA [1]
   AREA .chain .iteration .draw .value
  <dbl>  <int>      <int> <int>  <dbl>
1  462.     NA         NA     1   50.9
2  462.     NA         NA     2   44.0
3  462.     NA         NA     3   45.5
4  462.     NA         NA     4   46.7
5  462.     NA         NA     5   56.8
6  462.     NA         NA     6   43.8
ggplot(newdata, aes(y = .value, x = AREA)) +
  geom_line(aes(group = .draw), colour = "orange", alpha = 0.01) +
  geom_point(data = peake, aes(y = INDIV)) +
  scale_y_continuous("Number of Individuals") +
  scale_x_continuous(expression(Mussel ~ clump ~ area ~ (per ~ mm^2))) +
  theme_classic()

## Using emmeans
peake.grid <- with(peake, list(AREA = seq(min(AREA), max(AREA), len = 100)))

newdata <- emmeans(peake.brm4, ~AREA, at = peake.grid, type = "response") |> as.data.frame()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
head(newdata)
      AREA      prob lower.HPD upper.HPD
  462.2500  49.67395  31.33008  73.10291
  731.7629  72.59190  50.14352 101.36583
 1001.2759  93.95536  67.75712 126.00325
 1270.7888 114.44093  84.93029 148.25065
 1540.3017 134.09769 100.10843 168.20858
 1809.8146 153.39162 118.91091 191.06859

Point estimate displayed: median 
Results are back-transformed from the log scale 
HPD interval probability: 0.95 
ggplot(newdata, aes(y = prob, x = AREA)) +
  geom_point(data = peake, aes(y = INDIV)) +
  geom_line() +
  geom_ribbon(aes(ymin = lower.HPD, ymax = upper.HPD), fill = "blue", alpha = 0.3) +
  scale_y_continuous("Individuals") +
  scale_x_continuous("Mussel clump area") +
  theme_classic()

## spaghetti plot
newdata <- emmeans(peake.brm4, ~AREA, at = peake.grid) |>
  regrid() |>
  gather_emmeans_draws()
Warning: Method 'parse_bf' is deprecated. Please use 'brmsterms' instead.
newdata |> head()
# A tibble: 6 × 5
# Groups:   AREA [1]
   AREA .chain .iteration .draw .value
  <dbl>  <int>      <int> <int>  <dbl>
1  462.     NA         NA     1   48.2
2  462.     NA         NA     2   49.8
3  462.     NA         NA     3   37.5
4  462.     NA         NA     4   55.8
5  462.     NA         NA     5   50.4
6  462.     NA         NA     6   48.4
ggplot(newdata, aes(y = .value, x = AREA)) +
  geom_line(aes(group = .draw), colour = "orange", alpha = 0.01) +
  geom_point(data = peake, aes(y = INDIV)) +
  scale_y_continuous("Number of Individuals") +
  scale_x_continuous(expression(Mussel ~ clump ~ area ~ (per ~ mm^2))) +
  theme_classic()

13 References

Peake, A. J., and G. P. Quinn. 1993. “Temporal Variation in Species-Area Curves for Invertebrates in Clumps of an Intertidal Mussel.” Ecography 16: 269–77.